diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 4d1d2f3593949..92157ffc685ab 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -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'; @@ -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 @@ -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', @@ -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) { @@ -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(); @@ -217,29 +230,6 @@ async function initCommandLine() { return found; } - async function openDocsite(commandTemplate: string): Promise { - 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((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). * diff --git a/packages/aws-cdk/bin/commands/docs.ts b/packages/aws-cdk/bin/commands/docs.ts new file mode 100644 index 0000000000000..bdfc42058f166 --- /dev/null +++ b/packages/aws-cdk/bin/commands/docs.ts @@ -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((resolve, reject) => { + exec(browserCommand, (err, stdout, stderr) => { + if (err) { return reject(err); } + if (stdout) { debug(stdout); } + if (stderr) { warning(stderr); } + resolve(0); + }); + })); +} diff --git a/packages/aws-cdk/bin/commands/doctor.ts b/packages/aws-cdk/bin/commands/doctor.ts new file mode 100644 index 0000000000000..648deb589170a --- /dev/null +++ b/packages/aws-cdk/bin/commands/doctor.ts @@ -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; + } +} diff --git a/packages/aws-cdk/package-lock.json b/packages/aws-cdk/package-lock.json index 156c39ac9c2ca..0f05f2df0aa3c 100644 --- a/packages/aws-cdk/package-lock.json +++ b/packages/aws-cdk/package-lock.json @@ -1,16 +1,20 @@ { - "requires": true, + "name": "aws-cdk", + "version": "0.7.2-beta", "lockfileVersion": 1, + "requires": true, "dependencies": { "@types/caseless": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", - "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==" + "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==", + "dev": true }, "@types/form-data": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", + "dev": true, "requires": { "@types/node": "*" } @@ -19,6 +23,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-4.0.8.tgz", "integrity": "sha512-Z5nu9Pbxj9yNeXIK3UwGlRdJth4cZ5sCq05nI7FaI6B0oz28nxkOtp6Lsz0ZnmLHJGvOJfB/VHxSTbVq/i6ujA==", + "dev": true, "requires": { "@types/node": "*" } @@ -26,17 +31,26 @@ "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/mockery": { + "version": "1.4.29", + "resolved": "https://registry.npmjs.org/@types/mockery/-/mockery-1.4.29.tgz", + "integrity": "sha1-m6It838H43gP/4Ux0aOOYz+UV6U=", + "dev": true }, "@types/node": { "version": "8.10.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.17.tgz", - "integrity": "sha512-3N3FRd/rA1v5glXjb90YdYUa+sOB7WrkU2rAhKZnF4TKD86Cym9swtulGuH0p9nxo7fP5woRNa8b0oFTpCO1bg==" + "integrity": "sha512-3N3FRd/rA1v5glXjb90YdYUa+sOB7WrkU2rAhKZnF4TKD86Cym9swtulGuH0p9nxo7fP5woRNa8b0oFTpCO1bg==", + "dev": true }, "@types/request": { "version": "2.47.1", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.47.1.tgz", "integrity": "sha512-TV3XLvDjQbIeVxJ1Z3oCTDk/KuYwwcNKVwz2YaT0F5u86Prgc4syDAp6P96rkTQQ4bIdh+VswQIC9zS6NjY7/g==", + "dev": true, "requires": { "@types/caseless": "*", "@types/form-data": "*", @@ -47,12 +61,14 @@ "@types/tough-cookie": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha512-MDQLxNFRLasqS4UlkWMSACMKeSm1x4Q3TxzUC7KQUsh6RK1ZrQ0VEyE3yzXcBu+K8ejVj4wuX32eUG02yNp+YQ==" + "integrity": "sha512-MDQLxNFRLasqS4UlkWMSACMKeSm1x4Q3TxzUC7KQUsh6RK1ZrQ0VEyE3yzXcBu+K8ejVj4wuX32eUG02yNp+YQ==", + "dev": true }, "@types/uuid": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.3.tgz", "integrity": "sha512-5fRLCYhLtDb3hMWqQyH10qtF+Ud2JnNCXTCZ+9ktNdCcgslcuXkDTkFcJNk++MT29yDntDnlF1+jD+uVGumsbw==", + "dev": true, "requires": { "@types/node": "*" } @@ -60,12 +76,14 @@ "@types/yamljs": { "version": "0.2.30", "resolved": "https://registry.npmjs.org/@types/yamljs/-/yamljs-0.2.30.tgz", - "integrity": "sha1-0DTh0ynkbo0Pc3yajbl/aPgbU4I=" + "integrity": "sha1-0DTh0ynkbo0Pc3yajbl/aPgbU4I=", + "dev": true }, "@types/yargs": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-8.0.3.tgz", - "integrity": "sha512-YdxO7zGQf2qJeMgR0fNO8QTlj88L2zCP5GOddovoTyetgLiNDOUXcWzhWKb4EdZZlOjLQUA0JM8lW7VcKQL+9w==" + "integrity": "sha512-YdxO7zGQf2qJeMgR0fNO8QTlj88L2zCP5GOddovoTyetgLiNDOUXcWzhWKb4EdZZlOjLQUA0JM8lW7VcKQL+9w==", + "dev": true }, "ajv": { "version": "5.5.2", @@ -670,6 +688,12 @@ "brace-expansion": "^1.1.7" } }, + "mockery": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mockery/-/mockery-2.1.0.tgz", + "integrity": "sha512-9VkOmxKlWXoDO/h1jDZaS4lH33aWfRiJiNT/tKj+8OGzrcFDLo8d0syGdbsc3Bc4GvRXPb+NMMvojotmuGJTvA==", + "dev": true + }, "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index b467a704111bb..9287e28b1cbda 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -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", @@ -21,10 +22,14 @@ "devDependencies": { "@types/fs-extra": "^4.0.8", "@types/minimatch": "^3.0.3", + "@types/mockery": "^1.4.29", + "@types/nodeunit": "^0.0.30", "@types/request": "^2.47.1", "@types/uuid": "^3.4.3", "@types/yamljs": "^0.2.0", "@types/yargs": "^8.0.3", + "mockery": "^2.1.0", + "nodeunit": "^0.11.2", "pkglint": "^0.7.1" }, "dependencies": { diff --git a/packages/aws-cdk/test/test.cdk-docs.ts b/packages/aws-cdk/test/test.cdk-docs.ts new file mode 100644 index 0000000000000..e6918bd82b28f --- /dev/null +++ b/packages/aws-cdk/test/test.cdk-docs.ts @@ -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(); + } + } + } +}); diff --git a/packages/aws-cdk/test/test.cdk-doctor.ts b/packages/aws-cdk/test/test.cdk-doctor.ts new file mode 100644 index 0000000000000..4c55ad24b7720 --- /dev/null +++ b/packages/aws-cdk/test/test.cdk-doctor.ts @@ -0,0 +1,46 @@ +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(); + }, + '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' }); + + test.doesNotThrow(() => require('../bin/commands/doctor').handler()); + test.ok(exitCalled, 'process.exit() was called'); + test.equal(exitStatus, 0, 'exit status was 0'); + test.done(); + }, + 'exits with non-0 when documentation is missing'(test: Test) { + mockery.registerMock('process', { ...process, exit: fakeExit }); + + test.doesNotThrow(() => require('../bin/commands/doctor').handler()); + test.ok(exitCalled, 'process.exit() was called'); + test.notEqual(exitStatus, 0, 'exit status was non-0'); + test.done(); + } + } +});