create-oclif
Version:
oclif: create your own CLI
473 lines (472 loc) • 21.7 kB
JavaScript
"use strict";
const child_process_1 = require("child_process");
const fs = require("fs");
const _ = require("lodash");
const path = require("path");
const Generator = require("yeoman-generator");
const yosay = require("yosay");
const nps = require('nps-utils');
const sortPjson = require('sort-pjson');
const fixpack = require('@oclif/fixpack');
const debug = require('debug')('generator-oclif');
const { version } = require('../../package.json');
const isWindows = process.platform === 'win32';
const rmrf = isWindows ? 'rimraf' : 'rm -rf';
const rmf = isWindows ? 'rimraf' : 'rm -f';
let hasYarn = false;
try {
child_process_1.execSync('yarn -v', { stdio: 'ignore' });
hasYarn = true;
}
catch (_a) { }
// function stringToArray(s: string) {
// const keywords: string[] = []
// s.split(',').forEach((keyword: string) => {
// if (!keyword.length) {
// return false
// }
// return keywords.push(keyword.trim())
// })
// return keywords
// }
class App extends Generator {
constructor(args, opts) {
super(args, opts);
this.type = opts.type;
this.path = opts.path;
this.options = {
defaults: opts.defaults,
mocha: opts.options.includes('mocha'),
circleci: opts.options.includes('circleci'),
appveyor: opts.options.includes('appveyor'),
codecov: opts.options.includes('codecov'),
typescript: opts.options.includes('typescript'),
eslint: opts.options.includes('eslint'),
yarn: opts.options.includes('yarn') || hasYarn,
travisci: opts.options.includes('travisci'),
};
}
get _ext() {
return this.ts ? 'ts' : 'js';
}
get _bin() {
let bin = (this.pjson.oclif && (this.pjson.oclif.bin || this.pjson.oclif.dirname)) || this.pjson.name;
if (bin.includes('/'))
bin = bin.split('/').pop();
return bin;
}
// eslint-disable-next-line complexity
async prompting() {
let msg;
switch (this.type) {
case 'single':
msg = 'Time to build a single-command CLI with oclif!';
break;
case 'multi':
msg = 'Time to build a multi-command CLI with oclif!';
break;
default:
msg = `Time to build a oclif ${this.type}!`;
}
this.log(yosay(`${msg} Version: ${version}`));
if (this.path) {
this.destinationRoot(path.resolve(this.path));
process.chdir(this.destinationRoot());
}
this.githubUser = await this.user.github.username().catch(debug);
this.pjson = Object.assign({ scripts: {}, engines: {}, devDependencies: {}, dependencies: {}, oclif: {} }, this.fs.readJSON('package.json', {}));
let repository = this.destinationRoot().split(path.sep).slice(-2).join('/');
if (this.githubUser)
repository = `${this.githubUser}/${repository.split('/')[1]}`;
const defaults = Object.assign(Object.assign({ name: this.determineAppname().replace(/ /g, '-'), version: '0.0.0', license: 'MIT', author: this.githubUser ? `${this.user.git.name()} @${this.githubUser}` : this.user.git.name(), dependencies: {}, repository }, this.pjson), { engines: Object.assign({ node: '>=8.0.0' }, this.pjson.engines), options: this.options });
this.repository = defaults.repository;
if (this.repository && this.repository.url) {
this.repository = this.repository.url;
}
if (this.options.defaults) {
this.answers = defaults;
}
else {
this.answers = await this.prompt([
{
type: 'input',
name: 'name',
message: 'npm package name',
default: defaults.name,
when: !this.pjson.name,
},
{
type: 'input',
name: 'bin',
message: 'command bin name the CLI will export',
default: (answers) => (answers.name || this._bin).split('/').pop(),
when: ['single', 'multi'].includes(this.type) && !this.pjson.oclif.bin,
},
{
type: 'input',
name: 'description',
message: 'description',
default: defaults.description,
when: !this.pjson.description,
},
{
type: 'input',
name: 'author',
message: 'author',
default: defaults.author,
when: !this.pjson.author,
},
{
type: 'input',
name: 'version',
message: 'version',
default: defaults.version,
when: !this.pjson.version,
},
{
type: 'input',
name: 'license',
message: 'license',
default: defaults.license,
when: !this.pjson.license,
},
{
type: 'input',
name: 'github.user',
message: 'Who is the GitHub owner of repository (https://github.com/OWNER/repo)',
default: repository.split('/').slice(0, -1).pop(),
when: !this.pjson.repository,
},
{
type: 'input',
name: 'github.repo',
message: 'What is the GitHub name of repository (https://github.com/owner/REPO)',
default: (answers) => (this.pjson.repository || answers.name || this.pjson.name).split('/').pop(),
when: !this.pjson.repository,
},
{
type: 'list',
name: 'pkg',
message: 'Select a package manager',
choices: [
{ name: 'npm', value: 'npm' },
{ name: 'yarn', value: 'yarn' },
],
default: () => this.options.yarn || hasYarn ? 1 : 0,
},
{
type: 'confirm',
name: 'typescript',
message: 'TypeScript',
default: () => true,
},
{
type: 'confirm',
name: 'eslint',
message: 'Use eslint (linter for JavaScript and Typescript)',
default: () => true,
},
{
type: 'confirm',
name: 'mocha',
message: 'Use mocha (testing framework)',
default: () => true,
},
{
type: 'checkbox',
name: 'ci',
message: 'Add CI service config',
choices: [
{ name: 'circleci (continuous integration/delivery service)', value: 'circleci' },
{ name: 'appveyor (continuous integration/delivery service)', value: 'appveyor' },
{ name: 'codecov (online code coverage report viewer)', value: 'codecov' },
{ name: 'travisci (continuous integration/delivery service)', value: 'travisci' },
],
filter: ((arr) => _.keyBy(arr)),
},
]);
}
debug(this.answers);
if (!this.options.defaults) {
this.options = Object.assign(Object.assign({}, this.answers.ci), { mocha: this.answers.mocha, eslint: this.answers.eslint, typescript: this.answers.typescript, yarn: this.answers.pkg === 'yarn' });
}
this.ts = this.options.typescript;
this.yarn = this.options.yarn;
this.mocha = this.options.mocha;
this.circleci = this.options.circleci;
this.appveyor = this.options.appveyor;
this.codecov = this.options.codecov;
this.eslint = this.options.eslint;
this.travisci = this.options.travisci;
this.pjson.name = this.answers.name || defaults.name;
this.pjson.description = this.answers.description || defaults.description;
this.pjson.version = this.answers.version || defaults.version;
this.pjson.engines.node = defaults.engines.node;
this.pjson.author = this.answers.author || defaults.author;
this.pjson.files = this.answers.files || defaults.files || [(this.ts ? '/lib' : '/src')];
this.pjson.license = this.answers.license || defaults.license;
// eslint-disable-next-line no-multi-assign
this.repository = this.pjson.repository = this.answers.github ? `${this.answers.github.user}/${this.answers.github.repo}` : defaults.repository;
if (this.eslint) {
this.pjson.scripts.posttest = 'eslint .';
}
if (this.mocha) {
this.pjson.scripts.test = `nyc ${this.ts ? '--extension .ts ' : ''}mocha --forbid-only "test/**/*.test.${this._ext}"`;
}
else {
this.pjson.scripts.test = 'echo NO TESTS';
}
if (this.ts) {
this.pjson.scripts.prepack = nps.series(`${rmrf} lib`, 'tsc -b');
if (this.eslint) {
this.pjson.scripts.posttest = 'eslint . --ext .ts --config .eslintrc';
}
}
if (['plugin', 'multi'].includes(this.type)) {
this.pjson.scripts.prepack = nps.series(this.pjson.scripts.prepack, 'oclif-dev manifest', 'oclif-dev readme');
this.pjson.scripts.postpack = `${rmf} oclif.manifest.json`;
this.pjson.scripts.version = nps.series('oclif-dev readme', 'git add README.md');
this.pjson.files.push('/oclif.manifest.json');
this.pjson.files.push('/npm-shrinkwrap.json');
}
else if (this.type === 'single') {
this.pjson.scripts.prepack = nps.series(this.pjson.scripts.prepack, 'oclif-dev readme');
this.pjson.scripts.version = nps.series('oclif-dev readme', 'git add README.md');
}
if (this.type === 'plugin' && hasYarn) {
// for plugins, add yarn.lock file to package so we can lock plugin dependencies
this.pjson.files.push('/yarn.lock');
}
this.pjson.keywords = defaults.keywords || [this.type === 'plugin' ? 'oclif-plugin' : 'oclif'];
this.pjson.homepage = defaults.homepage || `https://github.com/${this.pjson.repository}`;
this.pjson.bugs = defaults.bugs || `https://github.com/${this.pjson.repository}/issues`;
if (['single', 'multi'].includes(this.type)) {
this.pjson.oclif.bin = this.answers.bin || this._bin;
this.pjson.bin = this.pjson.bin || {};
this.pjson.bin[this.pjson.oclif.bin] = './bin/run';
this.pjson.files.push('/bin');
}
else if (this.type === 'plugin') {
this.pjson.oclif.bin = 'oclif-example';
}
if (this.type !== 'plugin') {
this.pjson.main = defaults.main || (this.ts ? 'lib/index.js' : 'src/index.js');
if (this.ts) {
this.pjson.types = defaults.types || 'lib/index.d.ts';
}
}
}
// eslint-disable-next-line complexity
writing() {
this.sourceRoot(path.join(__dirname, '../../templates'));
switch (this.type) {
case 'multi':
case 'plugin':
this.pjson.oclif = Object.assign({ commands: `./${this.ts ? 'lib' : 'src'}/commands` }, this.pjson.oclif);
break;
default:
}
if (this.type === 'plugin' && !this.pjson.oclif.devPlugins) {
this.pjson.oclif.devPlugins = [
'@oclif/plugin-help',
];
}
if (this.type === 'multi' && !this.pjson.oclif.plugins) {
this.pjson.oclif.plugins = [
'@oclif/plugin-help',
];
}
if (this.pjson.oclif && Array.isArray(this.pjson.oclif.plugins)) {
this.pjson.oclif.plugins.sort();
}
if (this.ts) {
this.fs.copyTpl(this.templatePath('tsconfig.json'), this.destinationPath('tsconfig.json'), this);
if (this.mocha) {
this.fs.copyTpl(this.templatePath('test/tsconfig.json'), this.destinationPath('test/tsconfig.json'), this);
}
}
if (this.eslint) {
const eslintignore = this._eslintignore();
if (eslintignore.trim())
this.fs.write(this.destinationPath('.eslintignore'), this._eslintignore());
if (this.ts) {
this.fs.copyTpl(this.templatePath('eslintrc.typescript'), this.destinationPath('.eslintrc'), this);
}
else {
this.fs.copyTpl(this.templatePath('eslintrc'), this.destinationPath('.eslintrc'), this);
}
}
if (this.mocha) {
this.fs.copyTpl(this.templatePath('test/mocha.opts'), this.destinationPath('test/mocha.opts'), this);
}
if (this.fs.exists(this.destinationPath('./package.json'))) {
fixpack(this.destinationPath('./package.json'), require('@oclif/fixpack/config.json'));
}
if (_.isEmpty(this.pjson.oclif))
delete this.pjson.oclif;
this.pjson.files = _.uniq((this.pjson.files || []).sort());
this.fs.writeJSON(this.destinationPath('./package.json'), sortPjson(this.pjson));
this.fs.copyTpl(this.templatePath('editorconfig'), this.destinationPath('.editorconfig'), this);
if (this.circleci) {
this.fs.copyTpl(this.templatePath('circle.yml.ejs'), this.destinationPath('.circleci/config.yml'), this);
}
if (this.appveyor) {
this.fs.copyTpl(this.templatePath('appveyor.yml.ejs'), this.destinationPath('appveyor.yml'), this);
}
if (this.travisci) {
this.fs.copyTpl(this.templatePath('travis.yml.ejs'), this.destinationPath('.travis.yml'), this);
}
this.fs.copyTpl(this.templatePath('README.md.ejs'), this.destinationPath('README.md'), this);
if (this.pjson.license === 'MIT' && (this.pjson.repository.startsWith('oclif') || this.pjson.repository.startsWith('heroku'))) {
this.fs.copyTpl(this.templatePath('LICENSE.mit'), this.destinationPath('LICENSE'), this);
}
this.fs.write(this.destinationPath('.gitignore'), this._gitignore());
switch (this.type) {
case 'single':
this._writeSingle();
break;
case 'plugin':
this._writePlugin();
break;
case 'multi':
this._writeMulti();
break;
default:
this._writeBase();
}
}
install() {
const dependencies = [];
const devDependencies = [];
switch (this.type) {
case 'base': break;
case 'single':
dependencies.push('@oclif/config@^1', '@oclif/command@^1', '@oclif/plugin-help@^3');
devDependencies.push('@oclif/dev-cli@^1');
break;
case 'plugin':
dependencies.push('@oclif/command@^1', '@oclif/config@^1');
devDependencies.push('@oclif/dev-cli@^1', '@oclif/plugin-help@^3', 'globby@^10');
break;
case 'multi':
dependencies.push('@oclif/config@^1', '@oclif/command@^1', '@oclif/plugin-help@^3');
devDependencies.push('@oclif/dev-cli@^1', 'globby@^10');
}
if (this.mocha) {
devDependencies.push('mocha@^5', 'nyc@^14', 'chai@^4');
if (this.type !== 'base')
devDependencies.push('@oclif/test@^1');
}
if (this.ts) {
dependencies.push('tslib@^1');
devDependencies.push('@types/node@^10', 'typescript@^3.3', 'ts-node@^8');
if (this.mocha) {
devDependencies.push('@types/chai@^4', '@types/mocha@^5');
}
}
if (this.eslint) {
devDependencies.push('eslint@^5.13', 'eslint-config-oclif@^3.1');
if (this.ts) {
devDependencies.push('eslint-config-oclif-typescript@^0.1');
}
}
if (isWindows)
devDependencies.push('rimraf');
const yarnOpts = {};
if (process.env.YARN_MUTEX)
yarnOpts.mutex = process.env.YARN_MUTEX;
const install = (deps, opts) => this.yarn ? this.yarnInstall(deps, opts) : this.npmInstall(deps, opts);
const dev = this.yarn ? { dev: true } : { 'save-dev': true };
const save = this.yarn ? {} : { save: true };
return Promise.all([
install(devDependencies, Object.assign(Object.assign(Object.assign({}, yarnOpts), dev), { ignoreScripts: true })),
install(dependencies, Object.assign(Object.assign({}, yarnOpts), save)),
]).then(() => {
// if (!this.yarn) {
// return this.spawnCommand('npm', ['shrinkwrap'])
// }
});
}
end() {
if (['plugin', 'multi', 'single'].includes(this.type)) {
this.spawnCommandSync(path.join('.', 'node_modules/.bin/oclif-dev'), ['readme']);
}
console.log(`\nCreated ${this.pjson.name} in ${this.destinationRoot()}`);
}
_gitignore() {
const existing = this.fs.exists(this.destinationPath('.gitignore')) ? this.fs.read(this.destinationPath('.gitignore')).split('\n') : [];
return _([
'*-debug.log',
'*-error.log',
'node_modules',
'/tmp',
'/dist',
'/.nyc_output',
this.yarn ? '/package-lock.json' : '/yarn.lock',
this.ts && '/lib',
])
.concat(existing)
.compact()
.uniq()
.sort()
.join('\n') + '\n';
}
_eslintignore() {
const existing = this.fs.exists(this.destinationPath('.eslintignore')) ? this.fs.read(this.destinationPath('.eslintignore')).split('\n') : [];
return _([
this.ts && '/lib',
])
.concat(existing)
.compact()
.uniq()
.sort()
.join('\n') + '\n';
}
_writeBase() {
if (!fs.existsSync('src')) {
this.fs.copyTpl(this.templatePath(`base/src/index.${this._ext}`), this.destinationPath(`src/index.${this._ext}`), this);
}
if (this.mocha && !fs.existsSync('test')) {
this.fs.copyTpl(this.templatePath(`base/test/index.test.${this._ext}`), this.destinationPath(`test/index.test.${this._ext}`), this);
}
}
_writePlugin() {
const bin = this._bin;
const cmd = `${bin} hello`;
const opts = Object.assign(Object.assign({}, this), { _, bin, cmd });
this.fs.copyTpl(this.templatePath('plugin/bin/run'), this.destinationPath('bin/run'), opts);
this.fs.copyTpl(this.templatePath('bin/run.cmd'), this.destinationPath('bin/run.cmd'), opts);
const commandPath = this.destinationPath(`src/commands/hello.${this._ext}`);
if (!fs.existsSync('src/commands')) {
this.fs.copyTpl(this.templatePath(`src/command.${this._ext}.ejs`), commandPath, Object.assign(Object.assign({}, opts), { name: 'hello', path: commandPath.replace(process.cwd(), '.') }));
}
if (this.ts && this.type !== 'multi') {
this.fs.copyTpl(this.templatePath('plugin/src/index.ts'), this.destinationPath('src/index.ts'), opts);
}
if (this.mocha && !fs.existsSync('test')) {
this.fs.copyTpl(this.templatePath(`test/command.test.${this._ext}.ejs`), this.destinationPath(`test/commands/hello.test.${this._ext}`), Object.assign(Object.assign({}, opts), { name: 'hello' }));
}
}
_writeSingle() {
const bin = this._bin;
const opts = Object.assign(Object.assign({}, this), { _, bin, cmd: bin, name: this.pjson.name });
this.fs.copyTpl(this.templatePath(`single/bin/run.${this._ext}`), this.destinationPath('bin/run'), opts);
this.fs.copyTpl(this.templatePath('bin/run.cmd'), this.destinationPath('bin/run.cmd'), opts);
const commandPath = this.destinationPath(`src/index.${this._ext}`);
if (!this.fs.exists(`src/index.${this._ext}`)) {
this.fs.copyTpl(this.templatePath(`src/command.${this._ext}.ejs`), this.destinationPath(`src/index.${this._ext}`), Object.assign(Object.assign({}, opts), { path: commandPath.replace(process.cwd(), '.') }));
}
if (this.mocha && !this.fs.exists(`test/index.test.${this._ext}`)) {
this.fs.copyTpl(this.templatePath(`test/command.test.${this._ext}.ejs`), this.destinationPath(`test/index.test.${this._ext}`), opts);
}
}
_writeMulti() {
this._writePlugin();
this.fs.copyTpl(this.templatePath('bin/run'), this.destinationPath('bin/run'), this);
this.fs.copyTpl(this.templatePath('bin/run.cmd'), this.destinationPath('bin/run.cmd'), this);
if (!this.fs.exists(`src/index.${this._ext}`)) {
this.fs.copyTpl(this.templatePath(`multi/src/index.${this._ext}`), this.destinationPath(`src/index.${this._ext}`), this);
}
}
}
module.exports = App;