oclif
Version:
oclif: create your own CLI
308 lines (307 loc) • 13.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@oclif/core");
const debug_1 = __importDefault(require("debug"));
const ejs_1 = require("ejs");
const fs = __importStar(require("fs-extra"));
const promises_1 = require("node:fs/promises");
const node_path_1 = __importDefault(require("node:path"));
const node_url_1 = require("node:url");
const normalize_package_data_1 = __importDefault(require("normalize-package-data"));
const help_compatibility_1 = require("./help-compatibility");
const columns = Number.parseInt(process.env.COLUMNS, 10) || 120;
const util_1 = require("./util");
const debug = (0, debug_1.default)('readme');
async function slugify(str) {
const { default: GithubSlugger } = await import('github-slugger');
const slugify = new GithubSlugger();
return slugify.slug(str);
}
class ReadmeGenerator {
config;
options;
constructor(config, options) {
this.config = config;
this.options = options;
}
commandCode(c) {
if (this.options.sourceLinks === false)
return;
const pluginName = c.pluginName;
if (!pluginName)
return;
const plugin = this.config.plugins.get(pluginName);
if (!plugin)
return;
const repo = this.repo(plugin);
if (!repo)
return;
let label = plugin.name;
let version = plugin.version;
const commandPath = this.commandPath(plugin, c);
if (!commandPath)
return;
if (this.config.name === plugin.name) {
label = commandPath;
version = this.options.version || version;
}
const template = this.options.repositoryPrefix ||
plugin.pjson.oclif.repositoryPrefix ||
'<%- repo %>/blob/v<%- version %>/<%- commandPath %>';
return `_See code: [${label}](${(0, ejs_1.render)(template, { c, commandPath, config: this.config, repo, version })})_`;
}
async commands(commands) {
const helpClass = await (0, core_1.loadHelpClass)(this.config);
return [
...(await Promise.all(commands.map(async (c) => {
const usage = this.commandUsage(c);
return usage
? `* [\`${this.config.bin} ${usage}\`](#${await slugify(`${this.config.bin}-${usage}`)})`
: `* [\`${this.config.bin}\`](#${await slugify(`${this.config.bin}`)})`;
}))),
'',
...commands.map((c) => this.renderCommand({ ...c }, helpClass)).map((s) => s.trim() + '\n'),
]
.join('\n')
.trim();
}
async createTopicFile(file, topic, commands) {
const bin = `\`${this.config.bin} ${topic.name}\``;
const doc = [
bin,
'='.repeat(bin.length),
'',
(0, ejs_1.render)(topic.description || '', { config: this.config }).trim(),
'',
await this.commands(commands),
]
.join('\n')
.trim() + '\n';
await this.write(node_path_1.default.resolve(this.options.pluginDir ?? process.cwd(), file), doc);
}
async generate() {
let readme = await this.read();
const commands = (0, util_1.uniqBy)(this.config.commands
.filter((c) => !c.hidden && c.pluginType === 'core')
.filter((c) => (this.options.aliases ? true : !c.aliases.includes(c.id)))
.map((c) => (this.config.isSingleCommandCLI ? { ...c, id: '' } : c))
.sort((a, b) => a.id.localeCompare(b.id)), (c) => c.id);
debug('commands:', commands.map((c) => c.id).length);
readme = this.replaceTag(readme, 'usage', this.usage());
readme = this.replaceTag(readme, 'commands', this.options.multi
? await this.multiCommands(commands, this.options.outputDir, this.options.nestedTopicsDepth)
: await this.commands(commands));
readme = this.replaceTag(readme, 'toc', await this.tableOfContents(readme));
readme = readme.trimEnd();
readme += '\n';
await this.write(this.options.readmePath, readme);
return readme;
}
async multiCommands(commands, dir, nestedTopicsDepth) {
let topics = this.config.topics;
topics = nestedTopicsDepth
? topics.filter((t) => !t.hidden && (t.name.match(/:/g) || []).length < nestedTopicsDepth)
: topics.filter((t) => !t.hidden && !t.name.includes(':'));
topics = topics.filter((t) => commands.find((c) => c.id.startsWith(t.name)));
topics = (0, util_1.uniqBy)((0, util_1.sortBy)(topics, (t) => t.name), (t) => t.name);
for (const topic of topics) {
// eslint-disable-next-line no-await-in-loop
await this.createTopicFile(node_path_1.default.join('.', dir, topic.name.replaceAll(':', '/') + '.md'), topic, commands.filter((c) => c.id === topic.name || c.id.startsWith(topic.name + ':')));
}
return ([
'# Command Topics\n',
...topics.map((t) => (0, util_1.compact)([
`* [\`${this.config.bin} ${t.name.replaceAll(':', this.config.topicSeparator)}\`](${dir}/${t.name.replaceAll(':', '/')}.md)`,
(0, ejs_1.render)(t.description || '', { config: this.config })
.trim()
.split('\n')[0],
]).join(' - ')),
]
.join('\n')
.trim() + '\n');
}
async read() {
return (0, promises_1.readFile)(this.options.readmePath, 'utf8');
}
renderCommand(c, HelpClass) {
debug('rendering command', c.id);
const title = (0, ejs_1.render)(c.summary ?? c.description ?? '', { command: c, config: this.config })
.trim()
.split('\n')[0];
const help = new HelpClass(this.config, { maxWidth: columns, respectNoCacheDefault: true, stripAnsi: true });
const wrapper = new help_compatibility_1.HelpCompatibilityWrapper(help);
const header = () => {
const usage = this.commandUsage(c);
return usage ? `## \`${this.config.bin} ${usage}\`` : `## \`${this.config.bin}\``;
};
try {
// copy c to keep the command ID with colons, see:
// https://github.com/oclif/oclif/pull/1165#discussion_r1282305242
const command = { ...c };
return (0, util_1.compact)([
header(),
title,
'```\n' + wrapper.formatCommand(c).trim() + '\n```',
this.commandCode(command),
]).join('\n\n');
}
catch (error) {
const { message } = error;
core_1.ux.error(message);
}
}
replaceTag(readme, tag, body) {
if (readme.includes(`<!-- ${tag} -->`)) {
if (readme.includes(`<!-- ${tag}stop -->`)) {
readme = readme.replace(new RegExp(`<!-- ${tag} -->(.|\n)*<!-- ${tag}stop -->`, 'm'), `<!-- ${tag} -->`);
}
core_1.ux.stdout(`replacing <!-- ${tag} --> in ${this.options.readmePath}`);
}
return readme.replace(`<!-- ${tag} -->`, `<!-- ${tag} -->\n${body}\n<!-- ${tag}stop -->`);
}
async tableOfContents(readme) {
const toc = await Promise.all(readme
.split('\n')
.filter((l) => l.startsWith('# '))
.map((l) => l.trim().slice(2))
.map(async (l) => `* [${l}](#${await slugify(l)})`));
return toc.join('\n');
}
usage() {
const versionFlags = ['--version', ...(this.config.pjson.oclif.additionalVersionFlags ?? []).sort()];
const versionFlagsString = `(${versionFlags.join('|')})`;
return [
`\`\`\`sh-session
$ npm install -g ${this.config.name}
$ ${this.config.bin} COMMAND
running command...
$ ${this.config.bin} ${versionFlagsString}
${this.config.name}/${this.options.version || this.config.version} ${process.platform}-${process.arch} node-v${process.versions.node}
$ ${this.config.bin} --help [COMMAND]
USAGE
$ ${this.config.bin} COMMAND
...
\`\`\`\n`,
]
.join('\n')
.trim();
}
async write(file, content) {
if (!this.options.dryRun)
await fs.outputFile(file, content);
}
/**
* fetches the path to a command
*/
// eslint-disable-next-line complexity
commandPath(plugin, c) {
const strategy = typeof plugin.pjson.oclif?.commands === 'string' ? 'pattern' : plugin.pjson.oclif?.commands?.strategy;
// if the strategy is explicit, we can't determine the path so return undefined
if (strategy === 'explicit')
return;
const commandsDir = typeof plugin.pjson.oclif?.commands === 'string'
? plugin.pjson.oclif?.commands
: plugin.pjson.oclif?.commands?.target;
if (!commandsDir)
return;
const hasTypescript = plugin.pjson.devDependencies?.typescript || plugin.pjson.dependencies?.typescript;
let p = node_path_1.default.join(plugin.root, commandsDir, ...c.id.split(':'));
const outDir = node_path_1.default.dirname(commandsDir.replace(/^.\/|.\\/, '')); // remove leading ./ or .\ from path
const outDirRegex = new RegExp('^' + outDir + (node_path_1.default.sep === '\\' ? '\\\\' : node_path_1.default.sep));
if (fs.pathExistsSync(node_path_1.default.join(p, 'index.js'))) {
p = node_path_1.default.join(p, 'index.js');
}
else if (fs.pathExistsSync(p + '.js')) {
p += '.js';
}
else if (hasTypescript) {
// check if non-compiled scripts are available
const base = p.replace(plugin.root + node_path_1.default.sep, '');
p = node_path_1.default.join(plugin.root, base.replace(outDirRegex, 'src' + node_path_1.default.sep));
if (fs.pathExistsSync(node_path_1.default.join(p, 'index.ts'))) {
p = node_path_1.default.join(p, 'index.ts');
}
else if (fs.pathExistsSync(p + '.ts')) {
p += '.ts';
}
else
return;
}
else
return;
p = p.replace(plugin.root + node_path_1.default.sep, '');
if (hasTypescript) {
p = p.replace(outDirRegex, 'src' + node_path_1.default.sep).replace(/\.js$/, '.ts');
}
p = p.replaceAll('\\', '/'); // Replace windows '\' by '/'
return p;
}
commandUsage(command) {
const arg = (arg) => {
const name = arg.name.toUpperCase();
if (arg.required)
return `${name}`;
return `[${name}]`;
};
const id = (0, core_1.toConfiguredId)(command.id, this.config);
const defaultUsage = () => (0, util_1.compact)([
id,
Object.values(command.args)
.filter((a) => !a.hidden)
.map((a) => arg(a))
.join(' '),
]).join(' ');
const usages = (0, util_1.castArray)(command.usage);
return (0, ejs_1.render)(usages.length === 0 ? defaultUsage() : usages[0], { command, config: this.config });
}
repo(plugin) {
const pjson = { ...plugin.pjson };
(0, normalize_package_data_1.default)(pjson);
const repo = pjson.repository && pjson.repository.url;
if (!repo)
return;
const url = new node_url_1.URL(repo);
if (!['github.com', 'gitlab.com'].includes(url.hostname) &&
!pjson.oclif.repositoryPrefix &&
!this.options.repositoryPrefix)
return;
return `https://${url.hostname}${url.pathname.replace(/\.git$/, '')}`;
}
}
exports.default = ReadmeGenerator;