Skip to content

Commit

Permalink
Start re-factoring the CLI codebase
Browse files Browse the repository at this point in the history
Breaking down the monolithic CLI infrastructure by using `yarg`'s
`commandDir` directive (see #176). This allows modelling each command in
a separate module for a cleaner interface.

Migrated the `docs` command, and created a prototype of a `doctor`
command (see #154). The new commands also have basic unit tests that
verify the handlers honor their promises.
  • Loading branch information
RomainMuller committed Jun 25, 2018
1 parent 2059899 commit 2684cc4
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 39 deletions.
52 changes: 21 additions & 31 deletions packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import '../lib/api/util/sdk-load-aws-config';

import * as cxapi from '@aws-cdk/cx-api';
import { deepMerge, isEmpty, partition } from '@aws-cdk/util';
import { exec, spawn } from 'child_process';
import { spawn } from 'child_process';
import { blue, green } from 'colors/safe';
import * as fs from 'fs-extra';
import * as minimatch from 'minimatch';
Expand Down Expand Up @@ -50,10 +50,6 @@ async function parseCommandLineArguments() {
.option('json', { type: 'boolean', alias: 'j', desc: 'Use JSON output instead of YAML' })
.option('verbose', { type: 'boolean', alias: 'v', desc: 'Show debug logs' })
.demandCommand(1)
.command('docs', 'Opens the documentation in a browser', yargs => yargs
// tslint:disable-next-line:max-line-length
.option('browser', { type: 'string', alias: 'b', desc: 'the command to use to open the browser, using %u as a placeholder for the path of the file to open',
default: process.platform === 'win32' ? 'start %u' : 'open %u' }))
.command('list', 'Lists all stacks in the cloud executable (alias: ls)')
// tslint:disable-next-line:max-line-length
.command('synth [STACKS..]', 'Synthesizes and prints the cloud formation template for this stack (alias: synthesize, construct, cons)', yargs => yargs
Expand All @@ -73,6 +69,7 @@ async function parseCommandLineArguments() {
// tslint:disable-next-line:max-line-length
.option('language', { type: 'string', alias: 'l', desc: 'the language to be used for the new project (default can be configured in ~/.cdk.json)', choices: initTemplateLanuages })
.option('list', { type: 'boolean', desc: 'list the available templates' }))
.commandDir('./commands', { exclude: /^_.*/, visit: decorateCommand })
.version(VERSION)
.epilogue([
'If your app has a single stack, there is no need to specify the stack name',
Expand All @@ -82,6 +79,25 @@ async function parseCommandLineArguments() {
}
// tslint:enable:no-shadowed-variable

/**
* Decorates commands discovered by ``yargs.commandDir`` in order to apply global
* options as appropriate.
*
* @param commandObject is the command to be decorated.
* @returns a decorated ``CommandModule``.
*/
function decorateCommand(commandObject: yargs.CommandModule): yargs.CommandModule {
return {
...commandObject,
handler(args: yargs.Arguments) {
if (args.verbose) {
setVerbose();
}
return commandObject.handler(args);
}
};
}

async function initCommandLine() {
const argv = await parseCommandLineArguments();
if (argv.verbose) {
Expand Down Expand Up @@ -148,9 +164,6 @@ async function initCommandLine() {
const toolkitStackName = completeConfig().get(['toolkitStackName']) || DEFAULT_TOOLKIT_STACK_NAME;

switch (command) {
case 'docs':
return await openDocsite(completeConfig().get(['browser']));

case 'ls':
case 'list':
return await listStacks();
Expand Down Expand Up @@ -217,29 +230,6 @@ async function initCommandLine() {
return found;
}

async function openDocsite(commandTemplate: string): Promise<number> {
let documentationIndexPath: string;
try {
// tslint:disable-next-line:no-var-require Taking an un-declared dep on aws-cdk-docs, to avoid a dependency circle
const docs = require('aws-cdk-docs');
documentationIndexPath = docs.documentationIndexPath;
} catch (err) {
error('Unable to open CDK documentation - the aws-cdk-docs package appears to be missing. Please run `npm install -g aws-cdk-docs`');
return -1;
}

const browserCommand = commandTemplate.replace(/%u/g, documentationIndexPath);
debug(`Opening documentation ${green(browserCommand)}`);
return await new Promise<number>((resolve, reject) => {
exec(browserCommand, (err, stdout, stderr) => {
if (err) { return reject(err); }
if (stdout) { debug(stdout); }
if (stderr) { warning(stderr); }
resolve(0);
});
});
}

/**
* Bootstrap the CDK Toolkit stack in the accounts used by the specified stack(s).
*
Expand Down
45 changes: 45 additions & 0 deletions packages/aws-cdk/bin/commands/docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { exec } from 'child_process';
import { green } from 'colors/safe';
import * as process from 'process';
import * as yargs from 'yargs';
import { debug, error, warning } from '../../lib/logging';

export const command = 'docs';
export const describe = 'Opens the documentation in a browser';
export const aliases = ['doc'];
export const builder = {
browser: {
alias: 'b',
desc: 'the command to use to open the browser, using %u as a placeholder for the path of the file to open',
type: 'string',
default: process.platform === 'win32' ? 'start %u' : 'open %u'
}
};

export interface Arguments extends yargs.Arguments {
browser: string
}

export async function handler(argv: Arguments) {
let documentationIndexPath: string;
try {
// tslint:disable-next-line:no-var-require Taking an un-declared dep on aws-cdk-docs, to avoid a dependency circle
const docs = require('aws-cdk-docs');
documentationIndexPath = docs.documentationIndexPath;
} catch (err) {
error('Unable to open CDK documentation - the aws-cdk-docs package appears to be missing. Please run `npm install -g aws-cdk-docs`');
process.exit(-1);
return;
}

const browserCommand = argv.browser.replace(/%u/g, documentationIndexPath);
debug(`Opening documentation ${green(browserCommand)}`);
process.exit(await new Promise<number>((resolve, reject) => {
exec(browserCommand, (err, stdout, stderr) => {
if (err) { return reject(err); }
if (stdout) { debug(stdout); }
if (stderr) { warning(stderr); }
resolve(0);
});
}));
}
55 changes: 55 additions & 0 deletions packages/aws-cdk/bin/commands/doctor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { blue, green } from 'colors/safe';
import * as process from 'process';
import { print } from '../../lib/logging';
import { VERSION } from '../../lib/version';

export const command = 'doctor';
export const describe = 'Check your set-up for potential problems';
export const builder = {};

export function handler() {
let exitStatus: number = 0;
for (const verification of verifications) {
if (!verification()) {
exitStatus = -1;
}
}
process.exit(exitStatus);
}

const verifications: Array<() => boolean> = [
displayVersionInformation,
displayAwsEnvironmentVariables,
checkDocumentationIsAvailable
];

// ### Verifications ###

function displayVersionInformation() {
print(`ℹ️ CDK Version: ${green(VERSION)}`);
return true;
}

function displayAwsEnvironmentVariables() {
const keys = Object.keys(process.env).filter(s => s.startsWith('AWS_'));
if (keys.length === 0) {
print('ℹ️ No AWS environment variables');
return true;
}
print('ℹ️ AWS environment variables:');
for (const key of keys) {
print(` - ${blue(key)} = ${green(process.env[key]!)}`);
}
return true;
}

function checkDocumentationIsAvailable() {
try {
const version = require('aws-cdk-docs/package.json').version;
print(`✅ AWS CDK Documentation: ${version}`);
return true;
} catch (e) {
print(`❌ AWS CDK Documentation: install using ${green('y-npm install --global aws-cdk-docs')}`);
return false;
}
}
38 changes: 31 additions & 7 deletions packages/aws-cdk/package-lock.json

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

5 changes: 4 additions & 1 deletion packages/aws-cdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"prepare": "/bin/bash generate.sh && tslint -p . && tsc && chmod +x bin/cdk && pkglint",
"watch": "tsc -w",
"lint": "tsc && tslint -p . --force",
"pkglint": "pkglint -f"
"pkglint": "pkglint -f",
"test": "nodeunit test/test.*.js"
},
"author": {
"name": "Amazon Web Services",
Expand All @@ -21,10 +22,12 @@
"devDependencies": {
"@types/fs-extra": "^4.0.8",
"@types/minimatch": "^3.0.3",
"@types/mockery": "^1.4.29",
"@types/request": "^2.47.1",
"@types/uuid": "^3.4.3",
"@types/yamljs": "^0.2.0",
"@types/yargs": "^8.0.3",
"mockery": "^2.1.0",
"pkglint": "^0.7.1"
},
"dependencies": {
Expand Down
60 changes: 60 additions & 0 deletions packages/aws-cdk/test/test.cdk-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as mockery from 'mockery';
import { ICallbackFunction, Test, testCase } from 'nodeunit';

let exitCalled: boolean = false;
let exitStatus: undefined | number;
function fakeExit(status?: number) {
exitCalled = true;
exitStatus = status;
}

const argv = { browser: 'echo %u' };

module.exports = testCase({
'`cdk docs`': {
'setUp'(cb: ICallbackFunction) {
exitCalled = false;
exitStatus = undefined;
mockery.registerMock('../../lib/logging', {
debug() { return; },
error() { return; },
warning() { return; }
});
mockery.enable({ useCleanCache: true, warnOnReplace: true, warnOnUnregistered: false });
cb();
},
'tearDown'(cb: ICallbackFunction) {
mockery.disable();
mockery.deregisterAll();

cb();
},
async 'exits with 0 when everything is OK'(test: Test) {
mockery.registerMock('process', { ...process, exit: fakeExit });
mockery.registerMock('aws-cdk-docs', { documentationIndexPath: 'index.html' });

try {
await require('../bin/commands/docs').handler(argv);
test.ok(exitCalled, 'process.exit() was called');
test.equal(exitStatus, 0, 'exit status was 0');
} catch (e) {
test.ifError(e);
} finally {
test.done();
}
},
async 'exits with non-0 when documentation is missing'(test: Test) {
mockery.registerMock('process', { ...process, exit: fakeExit });

try {
await require('../bin/commands/docs').handler(argv);
test.ok(exitCalled, 'process.exit() was called');
test.notEqual(exitStatus, 0, 'exit status was non-0');
} catch (e) {
test.ifError(e);
} finally {
test.done();
}
}
}
});
Loading

0 comments on commit 2684cc4

Please sign in to comment.