refactor: make the cli testable

This commit is contained in:
Alan Agius 2018-09-14 09:34:53 +02:00 committed by Hans
parent 1753b6affb
commit 40129ccebd

View File

@ -17,7 +17,7 @@ import {
terminal, terminal,
virtualFs, virtualFs,
} from '@angular-devkit/core'; } from '@angular-devkit/core';
import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node'; import { NodeJsSyncHost, ProcessOutput, createConsoleLogger } from '@angular-devkit/core/node';
import { import {
DryRunEvent, DryRunEvent,
SchematicEngine, SchematicEngine,
@ -31,42 +31,6 @@ import {
import * as minimist from 'minimist'; import * as minimist from 'minimist';
/**
* Show usage of the CLI tool, and exit the process.
*/
function usage(exitCode = 0): never {
logger.info(tags.stripIndent`
schematics [CollectionName:]SchematicName [options, ...]
By default, if the collection name is not specified, use the internal collection provided
by the Schematics CLI.
Options:
--debug Debug mode. This is true by default if the collection is a relative
path (in that case, turn off with --debug=false).
--allowPrivate Allow private schematics to be run from the command line. Default to
false.
--dry-run Do not output anything, but instead just show what actions would be
performed. Default to true if debug is also true.
--force Force overwriting files that would otherwise be an error.
--list-schematics List all schematics from the collection, by name. A collection name
should be suffixed by a colon. Example: '@schematics/schematics:'.
--verbose Show more information.
--help Show this message.
Any additional option is passed to the Schematics depending on
`);
return process.exit(exitCode);
}
/** /**
* Parse the name of schematic passed in argument, and return a {collection, schematic} named * Parse the name of schematic passed in argument, and return a {collection, schematic} named
* tuple. The user can pass in `collection-name:schematic-name`, and this function will either * tuple. The user can pass in `collection-name:schematic-name`, and this function will either
@ -93,6 +57,18 @@ function parseSchematicName(str: string | null): { collection: string, schematic
} }
export interface MainOptions {
args: string[];
stdout?: ProcessOutput;
stderr?: ProcessOutput;
}
export async function main({
args,
stdout = process.stdout,
stderr = process.stderr,
}: MainOptions): Promise<0 | 1> {
/** Parse the command line. */ /** Parse the command line. */
const booleanArgs = [ const booleanArgs = [
'allowPrivate', 'allowPrivate',
@ -103,7 +79,7 @@ const booleanArgs = [
'list-schematics', 'list-schematics',
'verbose', 'verbose',
]; ];
const argv = minimist(process.argv.slice(2), { const argv = minimist(args, {
boolean: booleanArgs, boolean: booleanArgs,
default: { default: {
'debug': null, 'debug': null,
@ -113,10 +89,12 @@ const argv = minimist(process.argv.slice(2), {
}); });
/** Create the DevKit Logger used through the CLI. */ /** Create the DevKit Logger used through the CLI. */
const logger = createConsoleLogger(argv['verbose']); const logger = createConsoleLogger(argv['verbose'], stdout, stderr);
if (argv.help) { if (argv.help) {
usage(); logger.info(getUsage());
return 0;
} }
/** Get the collection an schematic name from the first argument. */ /** Get the collection an schematic name from the first argument. */
@ -137,12 +115,13 @@ if (argv['list-schematics']) {
const collection = engine.createCollection(collectionName); const collection = engine.createCollection(collectionName);
logger.info(engine.listSchematicNames(collection).join('\n')); logger.info(engine.listSchematicNames(collection).join('\n'));
process.exit(0); return 0;
} }
if (!schematicName) { if (!schematicName) {
usage(1); logger.info(getUsage());
throw 0; // TypeScript doesn't know that process.exit() never returns.
return 1;
} }
/** Gather the arguments for later use. */ /** Gather the arguments for later use. */
@ -225,10 +204,10 @@ workflow.lifeCycle.subscribe(event => {
/** /**
* Remove every options from argv that we support in schematics itself. * Remove every options from argv that we support in schematics itself.
*/ */
const args = Object.assign({}, argv); const parsedArgs = Object.assign({}, argv);
delete args['--']; delete parsedArgs['--'];
for (const key of booleanArgs) { for (const key of booleanArgs) {
delete args[key]; delete parsedArgs[key];
} }
/** /**
@ -236,7 +215,7 @@ for (const key of booleanArgs) {
*/ */
const argv2 = minimist(argv['--']); const argv2 = minimist(argv['--']);
for (const key of Object.keys(argv2)) { for (const key of Object.keys(argv2)) {
args[key] = argv2[key]; parsedArgs[key] = argv2[key];
} }
// Pass the rest of the arguments as the smart default "argv". Then delete it. // Pass the rest of the arguments as the smart default "argv". Then delete it.
@ -247,7 +226,7 @@ workflow.registry.addSmartDefaultProvider('argv', (schema: JsonObject) => {
return argv._; return argv._;
} }
}); });
delete args._; delete parsedArgs._;
/** /**
@ -258,17 +237,24 @@ delete args._;
* step of the workflow failed (sink or task), with details included, and will only complete * step of the workflow failed (sink or task), with details included, and will only complete
* when everything is done. * when everything is done.
*/ */
workflow.execute({ try {
await workflow.execute({
collection: collectionName, collection: collectionName,
schematic: schematicName, schematic: schematicName,
options: args, options: parsedArgs,
allowPrivate: allowPrivate, allowPrivate: allowPrivate,
debug: debug, debug: debug,
logger: logger, logger: logger,
}) })
.subscribe({ .toPromise();
error(err: Error) {
// In case the workflow was not successful, show an appropriate error message. if (nothingDone) {
logger.info('Nothing to be done.');
}
return 0;
} catch (err) {
if (err instanceof UnsuccessfulWorkflowExecution) { if (err instanceof UnsuccessfulWorkflowExecution) {
// "See above" because we already printed the error. // "See above" because we already printed the error.
logger.fatal('The Schematic workflow failed. See above.'); logger.fatal('The Schematic workflow failed. See above.');
@ -278,11 +264,46 @@ workflow.execute({
logger.fatal(err.stack || err.message); logger.fatal(err.stack || err.message);
} }
process.exit(1); return 1;
}, }
complete() { }
if (nothingDone) {
logger.info('Nothing to be done.'); /**
* Get usage of the CLI tool.
*/
function getUsage(): string {
return tags.stripIndent`
schematics [CollectionName:]SchematicName [options, ...]
By default, if the collection name is not specified, use the internal collection provided
by the Schematics CLI.
Options:
--debug Debug mode. This is true by default if the collection is a relative
path (in that case, turn off with --debug=false).
--allowPrivate Allow private schematics to be run from the command line. Default to
false.
--dry-run Do not output anything, but instead just show what actions would be
performed. Default to true if debug is also true.
--force Force overwriting files that would otherwise be an error.
--list-schematics List all schematics from the collection, by name. A collection name
should be suffixed by a colon. Example: '@schematics/schematics:'.
--verbose Show more information.
--help Show this message.
Any additional option is passed to the Schematics depending on
`;
}
if (require.main === module) {
const args = process.argv.slice(2);
main({ args })
.then(exitCode => process.exitCode = exitCode)
.catch(e => { throw (e); });
} }
},
});