mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-17 11:03:53 +08:00
Currently, upon execution `ng` will load all description files AND code for all available commands. This requires a large amount of unnecessary file access and processing since only at most one command will be executed. This change limits the loading to only command being executed in the common case and a subset of commands in the event an alias is used. The help command now loads all commands during its execution which is needed to gather command description information. Further improvements are possible by only loading the necessary metadata instead of the execution code (and its dependencies) as well. This change allows for savings of ~250ms per execution. Examples: Before -- `./node_modules/.bin/ng version 0.99s user 0.17s system 113% cpu 1.020 total` After -- `./node_modules/.bin/ng version 0.70s user 0.13s system 110% cpu 0.749 total` Before -- `./node_modules/.bin/ng g c a 1.91s user 0.30s system 111% cpu 1.996 total` After -- `./node_modules/.bin/ng g c a 1.62s user 0.27s system 110% cpu 1.715 total`
194 lines
5.9 KiB
TypeScript
194 lines
5.9 KiB
TypeScript
/**
|
|
* @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
|
|
*/
|
|
|
|
// tslint:disable:no-global-tslint-disable no-any
|
|
import { analytics, logging, strings, tags, terminal } from '@angular-devkit/core';
|
|
import * as path from 'path';
|
|
import { getWorkspace } from '../utilities/config';
|
|
import {
|
|
Arguments,
|
|
CommandContext,
|
|
CommandDescription,
|
|
CommandDescriptionMap,
|
|
CommandScope,
|
|
CommandWorkspace,
|
|
Option, SubCommandDescription,
|
|
} from './interface';
|
|
|
|
export interface BaseCommandOptions {
|
|
help?: boolean | string;
|
|
}
|
|
|
|
export abstract class Command<T extends BaseCommandOptions = BaseCommandOptions> {
|
|
public allowMissingWorkspace = false;
|
|
public workspace: CommandWorkspace;
|
|
public analytics: analytics.Analytics;
|
|
|
|
protected static commandMap: () => Promise<CommandDescriptionMap>;
|
|
static setCommandMap(map: () => Promise<CommandDescriptionMap>) {
|
|
this.commandMap = map;
|
|
}
|
|
|
|
constructor(
|
|
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<void> {
|
|
return;
|
|
}
|
|
|
|
async printHelp(options: T & Arguments): Promise<number> {
|
|
await this.printHelpUsage();
|
|
await this.printHelpOptions();
|
|
|
|
return 0;
|
|
}
|
|
|
|
async printJsonHelp(_options: T & Arguments): Promise<number> {
|
|
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(` ${terminal.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(` ${terminal.cyan('--' + strings.dasherize(o.name))} ${aliases}`);
|
|
if (o.description) {
|
|
this.logger.info(formatDescription(o.description));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async validateScope(scope?: CommandScope): Promise<void> {
|
|
switch (scope === undefined ? this.description.scope : scope) {
|
|
case CommandScope.OutProject:
|
|
if (this.workspace.configFile) {
|
|
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 "${path.join(
|
|
this.workspace.root,
|
|
this.workspace.configFile,
|
|
)}".
|
|
`);
|
|
throw 1;
|
|
}
|
|
break;
|
|
case CommandScope.InProject:
|
|
if (!this.workspace.configFile || getWorkspace('local') === null) {
|
|
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: T & Arguments,
|
|
dimensions: (boolean | number | string)[] = [],
|
|
metrics: (boolean | number | string)[] = [],
|
|
): Promise<void> {
|
|
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 });
|
|
}
|
|
|
|
abstract async run(options: T & Arguments): Promise<number | void>;
|
|
|
|
async validateAndRun(options: T & Arguments): Promise<number | void> {
|
|
if (!(options.help === true || options.help === 'json' || options.help === 'JSON')) {
|
|
await this.validateScope();
|
|
}
|
|
await this.initialize(options);
|
|
|
|
if (options.help === true) {
|
|
return this.printHelp(options);
|
|
} else if (options.help === 'json' || options.help === 'JSON') {
|
|
return this.printJsonHelp(options);
|
|
} else {
|
|
const startTime = +new Date();
|
|
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;
|
|
}
|
|
}
|
|
}
|