/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import { analytics, logging, strings, tags } from '@angular-devkit/core'; import { colors } from '../utilities/color'; import { AngularWorkspace } from '../utilities/config'; import { Arguments, CommandContext, CommandDescription, CommandDescriptionMap, CommandScope, Option, SubCommandDescription, } from './interface'; export interface BaseCommandOptions { help?: boolean | string; } export abstract class Command { protected allowMissingWorkspace = false; protected useReportAnalytics = true; readonly workspace?: AngularWorkspace; readonly analytics: analytics.Analytics; protected static commandMap: () => Promise; static setCommandMap(map: () => Promise) { this.commandMap = map; } constructor( protected readonly context: CommandContext, public readonly description: CommandDescription, protected readonly logger: logging.Logger, ) { this.workspace = context.workspace; this.analytics = context.analytics || new analytics.NoopAnalytics(); } async initialize(options: T & Arguments): Promise { return; } async printHelp(): Promise { await this.printHelpUsage(); await this.printHelpOptions(); return 0; } async printJsonHelp(_options: T & Arguments): Promise { this.logger.info(JSON.stringify(this.description)); return 0; } protected async printHelpUsage() { this.logger.info(this.description.description); const name = this.description.name; const args = this.description.options.filter(x => x.positional !== undefined); const opts = this.description.options.filter(x => x.positional === undefined); const argDisplay = args && args.length > 0 ? ' ' + args.map(a => `<${a.name}>`).join(' ') : ''; const optionsDisplay = opts && opts.length > 0 ? ` [options]` : ``; this.logger.info(`usage: ng ${name}${argDisplay}${optionsDisplay}`); this.logger.info(''); } protected async printHelpSubcommand(subcommand: SubCommandDescription) { this.logger.info(subcommand.description); await this.printHelpOptions(subcommand.options); } protected async printHelpOptions(options: Option[] = this.description.options) { const args = options.filter(opt => opt.positional !== undefined); const opts = options.filter(opt => opt.positional === undefined); const formatDescription = (description: string) => ` ${description.replace(/\n/g, '\n ')}`; if (args.length > 0) { this.logger.info(`arguments:`); args.forEach(o => { this.logger.info(` ${colors.cyan(o.name)}`); if (o.description) { this.logger.info(formatDescription(o.description)); } }); } if (options.length > 0) { if (args.length > 0) { this.logger.info(''); } this.logger.info(`options:`); opts .filter(o => !o.hidden) .sort((a, b) => a.name.localeCompare(b.name)) .forEach(o => { const aliases = o.aliases && o.aliases.length > 0 ? '(' + o.aliases.map(a => `-${a}`).join(' ') + ')' : ''; this.logger.info(` ${colors.cyan('--' + strings.dasherize(o.name))} ${aliases}`); if (o.description) { this.logger.info(formatDescription(o.description)); } }); } } async validateScope(scope?: CommandScope): Promise { switch (scope === undefined ? this.description.scope : scope) { case CommandScope.OutProject: if (this.workspace) { this.logger.fatal(tags.oneLine` The ${this.description.name} command requires to be run outside of a project, but a project definition was found at "${this.workspace.filePath}". `); throw 1; } break; case CommandScope.InProject: if (!this.workspace) { this.logger.fatal(tags.oneLine` The ${this.description.name} command requires to be run in an Angular project, but a project definition could not be found. `); throw 1; } break; case CommandScope.Everywhere: // Can't miss this. break; } } async reportAnalytics( paths: string[], options: Arguments, dimensions: (boolean | number | string)[] = [], metrics: (boolean | number | string)[] = [], ): Promise { for (const option of this.description.options) { const ua = option.userAnalytics; const v = options[option.name]; if (v !== undefined && !Array.isArray(v) && ua) { dimensions[ua] = v; } } this.analytics.pageview('/command/' + paths.join('/'), { dimensions, metrics }); } abstract run(options: T & Arguments): Promise; async validateAndRun(options: T & Arguments): Promise { if (!(options.help === true || options.help === 'json' || options.help === 'JSON')) { await this.validateScope(); } await this.initialize(options); if (options.help === true) { return this.printHelp(); } else if (options.help === 'json' || options.help === 'JSON') { return this.printJsonHelp(options); } else { const startTime = +new Date(); if (this.useReportAnalytics) { await this.reportAnalytics([this.description.name], options); } const result = await this.run(options); const endTime = +new Date(); this.analytics.timing(this.description.name, 'duration', endTime - startTime); return result; } } }