Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start re-factoring the CLI codebase #177

Merged
merged 1 commit into from
Jul 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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('../lib/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/lib/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/lib/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 async function handler(): Promise<never> {
let exitStatus: number = 0;
for (const verification of verifications) {
if (!await verification()) {
exitStatus = -1;
}
}
return process.exit(exitStatus);
}

const verifications: Array<() => boolean | Promise<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;
}
}
10 changes: 10 additions & 0 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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a note that this must be required first

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be required whenever... But I guess if you want to use it, you have to not require the stuff you want it to change how they are required before you've configured it... Which feels kind of obvious...

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('../lib/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('../lib/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();
}
}
}
});
56 changes: 56 additions & 0 deletions packages/aws-cdk/test/test.cdk-doctor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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;
}

module.exports = testCase({
'`cdk doctor`': {
'setUp'(cb: ICallbackFunction) {
exitCalled = false;
exitStatus = undefined;
mockery.registerMock('../../lib/logging', {
print: () => undefined
});
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/package.json', { version: 'x.y.z' });

try {
await require('../lib/commands/doctor').handler();
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('../lib/commands/doctor').handler();
test.ok(exitCalled, 'process.exit() was called');
test.notEqual(exitStatus, 0, 'exit status was non-0');
} catch (e) {
test.ifError(e);
} finally {
test.done();
}
}
}
});