Skip to content

Commit

Permalink
Added docs:edit command and support for subcommands
Browse files Browse the repository at this point in the history
  • Loading branch information
Dom Harrington committed May 18, 2018
1 parent 410b3b6 commit 36b2059
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 10 deletions.
14 changes: 10 additions & 4 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ process.env.NODE_CONFIG_DIR = configDir;

const { version } = require('./package.json');

function load(command = 'help') {
const file = path.join(__dirname, 'lib', `${command}.js`);
function load(command = 'help', subcommand = '') {
const file = path.join(__dirname, 'lib', command, subcommand);
try {
// eslint-disable-next-line global-require, import/no-dynamic-require
return require(file);
Expand All @@ -30,9 +30,15 @@ function load(command = 'help') {
module.exports = function(cmd, args, opts = {}) {
if (opts.version && !cmd) return Promise.resolve(version);

let command = cmd || '';
let subcommand;

if (command.includes(':')) {
[command, subcommand] = cmd.split(':');
}

try {
const command = load(opts.help ? 'help' : cmd);
return command.run({ args, opts });
return load(opts.help ? 'help' : command, subcommand).run({ args, opts });
} catch (e) {
return Promise.reject(e);
}
Expand Down
81 changes: 81 additions & 0 deletions lib/docs/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const request = require('request-promise-native');

const config = require('config');
const fs = require('fs');
const editor = require('editor');
const { promisify } = require('util');

const writeFile = promisify(fs.writeFile);
const readFile = promisify(fs.readFile);
const unlink = promisify(fs.unlink);

exports.desc = 'Edit a single file from your ReadMe project without saving locally';
exports.category = 'services';
exports.weight = 3;
exports.action = 'docs:edit';

exports.run = async function({ args, opts }) {
const { key, version } = opts;

if (!key) {
return Promise.reject(new Error('No api key provided. Please use --key'));
}

if (!version) {
return Promise.reject(new Error('No version provided. Please use --version'));
}

if (!args[0]) {
return Promise.reject(new Error('No slug provided. Usage `rdme docs:edit <slug>`'));
}

const slug = args[0];
const filename = `${slug}.md`;

const options = {
auth: { user: key },
headers: {
'x-readme-version': version,
},
};

const existingDoc = await request.get(`${config.host}/api/v1/docs/${slug}`, {
json: true,
...options,
}).catch(err => {
if (err.statusCode === 404) {
return Promise.reject(err.error);
}

return Promise.reject(err);
});

await writeFile(filename, existingDoc.body);

return new Promise((resolve, reject) => {
(opts.mockEditor || editor)(filename, async code => {
if (code !== 0) return reject(new Error('Non zero exit code from $EDITOR'));
const updatedDoc = await readFile(filename, 'utf8');

return request
.put(`${config.host}/api/v1/docs/${slug}`, {
json: Object.assign(existingDoc, {
body: updatedDoc,
}),
...options,
})
.then(async () => {
console.log('Doc successfully updated. Cleaning up local file');
await unlink(filename);
return resolve();
})
.catch(err => {
if (err.statusCode === 400) {
return reject(err.error);
}

return reject(err);
});
});
});
};
1 change: 1 addition & 0 deletions lib/docs.js → lib/docs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const readFile = promisify(fs.readFile);
exports.desc = 'Sync a folder of markdown files to your ReadMe project';
exports.category = 'services';
exports.weight = 2;
exports.action = 'docs';

exports.run = function({ args, opts }) {
const { key, version } = opts;
Expand Down
12 changes: 10 additions & 2 deletions lib/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ exports.run = function() {
console.log(`Usage: ${config.cli} <command> [arguments]`);
const files = fs
.readdirSync(__dirname)
.map(file => {
const stats = fs.statSync(path.join(__dirname, file));
if (stats.isDirectory()) {
return fs.readdirSync(path.join(__dirname, file)).map(f => path.join(file, f));
}
return [file];
})
.reduce((a, b) => a.concat(b), [])
.filter(file => file.endsWith('.js'))
.map(file => path.join(__dirname, file));

Expand All @@ -30,14 +38,14 @@ exports.run = function() {
};

files.forEach(file => {
const action = file.match(/(\w+).js/)[1];
const action = path.basename(file, '.js');
// eslint-disable-next-line global-require, import/no-dynamic-require
const f = require(file);
const info = f.desc || '';

if (f.category) {
categories[f.category].commands.push({
text: `${' $'.grey + pad(` ${config.cli} ${action}`)} ${info.grey}`,
text: `${' $'.grey + pad(` ${config.cli} ${f.action || action}`)} ${info.grey}`,
weight: f.weight,
});
}
Expand Down
2 changes: 1 addition & 1 deletion lib/oas.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const spawn = promisify(cp.spawn);

exports.desc = 'OAS related tasks. See https://www.npmjs.com/package/oas';
exports.category = 'services';
exports.weight = 3;
exports.weight = 4;

exports.run = function() {
return spawn(path.join(__dirname, '..', 'node_modules', '.bin', 'oas'), process.argv.slice(3), {
Expand Down
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"dependencies": {
"colors": "^1.1.2",
"config": "^1.30.0",
"editor": "^1.0.0",
"gray-matter": "^4.0.1",
"minimist": "^1.2.0",
"oas": "^0.6.7",
Expand Down Expand Up @@ -56,10 +57,10 @@
],
"coverageThreshold": {
"global": {
"branches": 40,
"branches": 70,
"functions": 80,
"lines": 80,
"statements": 75
"lines": 90,
"statements": 90
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions test/cli.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const assert = require('assert');
const minimist = require('minimist');
const nock = require('nock');

const cli = require('../cli');
const { version } = require('../package.json');
Expand Down Expand Up @@ -30,4 +31,22 @@ describe('cli', () => {
describe('--help', () => {
it('should print help and not error', () => cli('', [], minimist(['--help'])));
});

describe('subcommands', () => {
// docs:edit will make a backend connection
beforeAll(() => nock.disableNetConnect());

it('should load subcommands from the folder', () =>
cli('docs:edit', ['getting-started'], minimist(['--version=1.0.0', '--key=abcdef'])).catch(
e => {
assert.notEqual(e.message, 'Command not found.');
},
));
});

it('should not error with undefined cmd', () =>
cli(undefined, []).catch(() => {
// This can be ignored as it's just going to be
// a command not found error
}));
});
112 changes: 112 additions & 0 deletions test/docs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const crypto = require('crypto');
const frontMatter = require('gray-matter');

const docs = require('../cli').bind(null, 'docs');
const docsEdit = require('../cli').bind(null, 'docs:edit');

const key = 'Xmw4bGctRVIQz7R7dQXqH9nQe5d0SPQs';
const version = '1.0.0';
Expand Down Expand Up @@ -135,3 +136,114 @@ describe('docs command', () => {
});
});
});

describe('docs:edit', () => {
it('should error if no api key provided', () =>
docsEdit([], {}).catch(err => {
assert.equal(err.message, 'No api key provided. Please use --key');
}));

it('should error if no version provided', () =>
docsEdit([], { key }).catch(err => {
assert.equal(err.message, 'No version provided. Please use --version');
}));

it('should error if no slug provided', () =>
docsEdit([], { key, version: '1.0.0' }).catch(err => {
assert.equal(err.message, 'No slug provided. Usage `rdme docs:edit <slug>`');
}));

it('should fetch the doc from the api', () => {
const slug = 'getting-started';
const body = 'abcdef';
const edits = 'ghijkl';

const getMock = nock(config.host, {
reqheaders: {
'x-readme-version': version,
},
})
.get(`/api/v1/docs/${slug}`)
.basicAuth({ user: key })
.reply(200, { category: '5ae9ece93a685f47efb9a97c', slug, body });

const putMock = nock(config.host, {
reqheaders: {
'x-readme-version': version,
},
})
.put(`/api/v1/docs/${slug}`, {
category: '5ae9ece93a685f47efb9a97c',
slug,
body: `${body}${edits}`,
})
.basicAuth({ user: key })
.reply(200);

function mockEditor(filename, cb) {
assert.equal(filename, `${slug}.md`);
assert.equal(fs.existsSync(filename), true);
fs.appendFile(filename, edits, cb.bind(null, 0));
}

return docsEdit([slug], { key, version: '1.0.0', mockEditor }).then(() => {
getMock.done();
putMock.done();
assert.equal(fs.existsSync(`${slug}.md`), false);
});
});

it('should error if remote doc does not exist', () => {
const slug = 'no-such-doc';

const getMock = nock(config.host)
.get(`/api/v1/docs/${slug}`)
.reply(404, { error: 'Not Found', description: 'No doc found with that slug' });

return docsEdit([slug], { key, version: '1.0.0' }).catch(err => {
getMock.done();
assert.equal(err.error, 'Not Found');
assert.equal(err.description, 'No doc found with that slug');
});
});

it('should error if doc fails validation', () => {
const slug = 'getting-started';

const getMock = nock(config.host)
.get(`/api/v1/docs/${slug}`)
.reply(200, {});

const putMock = nock(config.host)
.put(`/api/v1/docs/${slug}`)
.reply(400, { error: 'Bad Request' });

function mockEditor(filename, cb) {
return cb(0);
}

return docsEdit([slug], { key, version: '1.0.0', mockEditor }).catch((err) => {
assert.equal(err.error, 'Bad Request');
getMock.done();
putMock.done();
assert.equal(fs.existsSync(`${slug}.md`), true);
fs.unlinkSync(`${slug}.md`);
});
});

it('should handle error if $EDITOR fails', () => {
const slug = 'getting-started';
nock(config.host)
.get(`/api/v1/docs/${slug}`)
.reply(200, {})

function mockEditor(filename, cb) {
return cb(1);
}

return docsEdit([slug], { key, version: '1.0.0', mockEditor }).catch((err) => {
assert.equal(err.message, 'Non zero exit code from $EDITOR');
fs.unlinkSync(`${slug}.md`);
});
});
});

0 comments on commit 36b2059

Please sign in to comment.