UNPKG

9.94 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3// tslint:disable no-implicit-dependencies
4const command_1 = require("@oclif/command");
5const Config = require("@oclif/config");
6const plugin_help_1 = require("@oclif/plugin-help");
7const fs = require("fs-extra");
8const _ = require("lodash");
9const path = require("path");
10const url_1 = require("url");
11const util_1 = require("../util");
12const help_compatibility_1 = require("../help-compatibility");
13const normalize = require('normalize-package-data');
14const columns = parseInt(process.env.COLUMNS, 10) || 120;
15const slugify = new (require('github-slugger'))();
16class Readme extends command_1.Command {
17 async run() {
18 const { flags } = this.parse(Readme);
19 const cwd = process.cwd();
20 const readmePath = path.resolve(cwd, 'README.md');
21 const config = await Config.load({ root: cwd, devPlugins: false, userPlugins: false });
22 try {
23 const p = require.resolve('@oclif/plugin-legacy', { paths: [cwd] });
24 const plugin = new Config.Plugin({ root: p, type: 'core' });
25 await plugin.load();
26 config.plugins.push(plugin);
27 }
28 catch (_a) { }
29 await config.runHook('init', { id: 'readme', argv: this.argv });
30 let readme = await fs.readFile(readmePath, 'utf8');
31 let commands = config.commands;
32 commands = commands.filter(c => !c.hidden);
33 commands = commands.filter(c => c.pluginType === 'core');
34 this.debug('commands:', commands.map(c => c.id).length);
35 commands = util_1.uniqBy(commands, c => c.id);
36 commands = util_1.sortBy(commands, c => c.id);
37 readme = this.replaceTag(readme, 'usage', this.usage(config));
38 readme = this.replaceTag(readme, 'commands', flags.multi ? this.multiCommands(config, commands, flags.dir) : this.commands(config, commands));
39 readme = this.replaceTag(readme, 'toc', this.toc(config, readme));
40 readme = readme.trimRight();
41 readme += '\n';
42 await fs.outputFile(readmePath, readme);
43 }
44 replaceTag(readme, tag, body) {
45 if (readme.includes(`<!-- ${tag} -->`)) {
46 if (readme.includes(`<!-- ${tag}stop -->`)) {
47 readme = readme.replace(new RegExp(`<!-- ${tag} -->(.|\n)*<!-- ${tag}stop -->`, 'm'), `<!-- ${tag} -->`);
48 }
49 this.log(`replacing <!-- ${tag} --> in README.md`);
50 }
51 return readme.replace(`<!-- ${tag} -->`, `<!-- ${tag} -->\n${body}\n<!-- ${tag}stop -->`);
52 }
53 toc(__, readme) {
54 return readme.split('\n').filter(l => l.startsWith('# '))
55 .map(l => l.trim().slice(2))
56 .map(l => `* [${l}](#${slugify.slug(l)})`)
57 .join('\n');
58 }
59 usage(config) {
60 return [
61 `\`\`\`sh-session
62$ npm install -g ${config.name}
63$ ${config.bin} COMMAND
64running command...
65$ ${config.bin} (-v|--version|version)
66${config.name}/${process.env.OCLIF_NEXT_VERSION || config.version} ${process.platform}-${process.arch} node-v${process.versions.node}
67$ ${config.bin} --help [COMMAND]
68USAGE
69 $ ${config.bin} COMMAND
70...
71\`\`\`\n`,
72 ].join('\n').trim();
73 }
74 multiCommands(config, commands, dir) {
75 let topics = config.topics;
76 topics = topics.filter(t => !t.hidden && !t.name.includes(':'));
77 topics = topics.filter(t => commands.find(c => c.id.startsWith(t.name)));
78 topics = util_1.sortBy(topics, t => t.name);
79 topics = util_1.uniqBy(topics, t => t.name);
80 for (const topic of topics) {
81 this.createTopicFile(path.join('.', dir, topic.name.replace(/:/g, '/') + '.md'), config, topic, commands.filter(c => c.id === topic.name || c.id.startsWith(topic.name + ':')));
82 }
83 return [
84 '# Command Topics\n',
85 ...topics.map(t => {
86 return util_1.compact([
87 `* [\`${config.bin} ${t.name}\`](${dir}/${t.name.replace(/:/g, '/')}.md)`,
88 util_1.template({ config })(t.description || '').trim().split('\n')[0],
89 ]).join(' - ');
90 }),
91 ].join('\n').trim() + '\n';
92 }
93 createTopicFile(file, config, topic, commands) {
94 const bin = `\`${config.bin} ${topic.name}\``;
95 const doc = [
96 bin,
97 '='.repeat(bin.length),
98 '',
99 util_1.template({ config })(topic.description || '').trim(),
100 '',
101 this.commands(config, commands),
102 ].join('\n').trim() + '\n';
103 fs.outputFileSync(file, doc);
104 }
105 commands(config, commands) {
106 return [
107 ...commands.map(c => {
108 const usage = this.commandUsage(config, c);
109 return `* [\`${config.bin} ${usage}\`](#${slugify.slug(`${config.bin}-${usage}`)})`;
110 }),
111 '',
112 ...commands.map(c => this.renderCommand(config, c)).map(s => s.trim() + '\n'),
113 ].join('\n').trim();
114 }
115 renderCommand(config, c) {
116 this.debug('rendering command', c.id);
117 const title = util_1.template({ config, command: c })(c.description || '').trim().split('\n')[0];
118 const HelpClass = plugin_help_1.getHelpClass(config);
119 const help = new HelpClass(config, { stripAnsi: true, maxWidth: columns });
120 const wrapper = new help_compatibility_1.HelpCompatibilityWrapper(help);
121 const header = () => `## \`${config.bin} ${this.commandUsage(config, c)}\``;
122 try {
123 return util_1.compact([
124 header(),
125 title,
126 '```\n' + wrapper.formatCommand(c).trim() + '\n```',
127 this.commandCode(config, c),
128 ]).join('\n\n');
129 }
130 catch (error) {
131 this.error(error.message);
132 }
133 }
134 commandCode(config, c) {
135 const pluginName = c.pluginName;
136 if (!pluginName)
137 return;
138 const plugin = config.plugins.find(p => p.name === c.pluginName);
139 if (!plugin)
140 return;
141 const repo = this.repo(plugin);
142 if (!repo)
143 return;
144 let label = plugin.name;
145 let version = plugin.version;
146 const commandPath = this.commandPath(plugin, c);
147 if (!commandPath)
148 return;
149 if (config.name === plugin.name) {
150 label = commandPath;
151 version = process.env.OCLIF_NEXT_VERSION || version;
152 }
153 const template = plugin.pjson.oclif.repositoryPrefix || '<%- repo %>/blob/v<%- version %>/<%- commandPath %>';
154 return `_See code: [${label}](${_.template(template)({ repo, version, commandPath, config, c })})_`;
155 }
156 repo(plugin) {
157 const pjson = Object.assign({}, plugin.pjson);
158 normalize(pjson);
159 const repo = pjson.repository && pjson.repository.url;
160 if (!repo)
161 return;
162 const url = new url_1.URL(repo);
163 if (!['github.com', 'gitlab.com'].includes(url.hostname) && !pjson.oclif.repositoryPrefix)
164 return;
165 return `https://${url.hostname}${url.pathname.replace(/\.git$/, '')}`;
166 }
167 // eslint-disable-next-line valid-jsdoc
168 /**
169 * fetches the path to a command
170 */
171 commandPath(plugin, c) {
172 const commandsDir = plugin.pjson.oclif.commands;
173 if (!commandsDir)
174 return;
175 let p = path.join(plugin.root, commandsDir, ...c.id.split(':'));
176 const libRegex = new RegExp('^lib' + (path.sep === '\\' ? '\\\\' : path.sep));
177 if (fs.pathExistsSync(path.join(p, 'index.js'))) {
178 p = path.join(p, 'index.js');
179 }
180 else if (fs.pathExistsSync(p + '.js')) {
181 p += '.js';
182 }
183 else if (plugin.pjson.devDependencies && plugin.pjson.devDependencies.typescript) {
184 // check if non-compiled scripts are available
185 const base = p.replace(plugin.root + path.sep, '');
186 p = path.join(plugin.root, base.replace(libRegex, 'src' + path.sep));
187 if (fs.pathExistsSync(path.join(p, 'index.ts'))) {
188 p = path.join(p, 'index.ts');
189 }
190 else if (fs.pathExistsSync(p + '.ts')) {
191 p += '.ts';
192 }
193 else
194 return;
195 }
196 else
197 return;
198 p = p.replace(plugin.root + path.sep, '');
199 if (plugin.pjson.devDependencies && plugin.pjson.devDependencies.typescript) {
200 p = p.replace(libRegex, 'src' + path.sep);
201 p = p.replace(/\.js$/, '.ts');
202 }
203 p = p.replace(/\\/g, '/'); // Replace windows '\' by '/'
204 return p;
205 }
206 commandUsage(config, command) {
207 const arg = (arg) => {
208 const name = arg.name.toUpperCase();
209 if (arg.required)
210 return `${name}`;
211 return `[${name}]`;
212 };
213 const defaultUsage = () => {
214 // const flags = Object.entries(command.flags)
215 // .filter(([, v]) => !v.hidden)
216 return util_1.compact([
217 command.id,
218 command.args.filter(a => !a.hidden).map(a => arg(a)).join(' '),
219 ]).join(' ');
220 };
221 const usages = util_1.castArray(command.usage);
222 return util_1.template({ config, command })(usages.length === 0 ? defaultUsage() : usages[0]);
223 }
224}
225exports.default = Readme;
226Readme.description = `adds commands to README.md in current directory
227The readme must have any of the following tags inside of it for it to be replaced or else it will do nothing:
228# Usage
229<!-- usage -->
230# Commands
231<!-- commands -->
232
233Customize the code URL prefix by setting oclif.repositoryPrefix in package.json.
234`;
235Readme.flags = {
236 dir: command_1.flags.string({ description: 'output directory for multi docs', default: 'docs', required: true }),
237 multi: command_1.flags.boolean({ description: 'create a different markdown page for each topic' }),
238};