Skip to content

Commit

Permalink
perf(@angular/cli): register CLI commands lazily
Browse files Browse the repository at this point in the history
Currently there is a lot of overhead coming from requiring external modules when registering commands such as `ng update` and `ng add`.
This is because these commands do not lazily require all the modules causes the resolution of unneeded packages to be part of the critical path.

With this change we "require” only the command that we we need to execute, which reduce the number of node modules resolutions in the critical path.

(cherry picked from commit 5b62074)
  • Loading branch information
alan-agius4 authored and dgp1130 committed Apr 13, 2023
1 parent 4d81cb4 commit f9b2fb1
Show file tree
Hide file tree
Showing 22 changed files with 204 additions and 76 deletions.
75 changes: 32 additions & 43 deletions packages/angular/cli/src/command-builder/command-runner.ts
Expand Up @@ -9,56 +9,25 @@
import { logging } from '@angular-devkit/core';
import yargs from 'yargs';
import { Parser } from 'yargs/helpers';
import { AddCommandModule } from '../commands/add/cli';
import { AnalyticsCommandModule } from '../commands/analytics/cli';
import { BuildCommandModule } from '../commands/build/cli';
import { CacheCommandModule } from '../commands/cache/cli';
import { CompletionCommandModule } from '../commands/completion/cli';
import { ConfigCommandModule } from '../commands/config/cli';
import { DeployCommandModule } from '../commands/deploy/cli';
import { DocCommandModule } from '../commands/doc/cli';
import { E2eCommandModule } from '../commands/e2e/cli';
import { ExtractI18nCommandModule } from '../commands/extract-i18n/cli';
import { GenerateCommandModule } from '../commands/generate/cli';
import { LintCommandModule } from '../commands/lint/cli';
import { AwesomeCommandModule } from '../commands/make-this-awesome/cli';
import { NewCommandModule } from '../commands/new/cli';
import { RunCommandModule } from '../commands/run/cli';
import { ServeCommandModule } from '../commands/serve/cli';
import { TestCommandModule } from '../commands/test/cli';
import { UpdateCommandModule } from '../commands/update/cli';
import { VersionCommandModule } from '../commands/version/cli';
import {
CommandConfig,
CommandNames,
RootCommands,
RootCommandsAliases,
} from '../commands/command-config';
import { colors } from '../utilities/color';
import { AngularWorkspace, getWorkspace } from '../utilities/config';
import { assertIsError } from '../utilities/error';
import { PackageManagerUtils } from '../utilities/package-manager';
import { CommandContext, CommandModuleError } from './command-module';
import { addCommandModuleToYargs, demandCommandFailureMessage } from './utilities/command';
import {
CommandModuleConstructor,
addCommandModuleToYargs,
demandCommandFailureMessage,
} from './utilities/command';
import { jsonHelpUsage } from './utilities/json-help';
import { normalizeOptionsMiddleware } from './utilities/normalize-options-middleware';

const COMMANDS = [
VersionCommandModule,
DocCommandModule,
AwesomeCommandModule,
ConfigCommandModule,
AnalyticsCommandModule,
AddCommandModule,
GenerateCommandModule,
BuildCommandModule,
E2eCommandModule,
TestCommandModule,
ServeCommandModule,
ExtractI18nCommandModule,
DeployCommandModule,
LintCommandModule,
NewCommandModule,
UpdateCommandModule,
RunCommandModule,
CacheCommandModule,
CompletionCommandModule,
].sort(); // Will be sorted by class name.

const yargsParser = Parser as unknown as typeof Parser.default;

export async function runCommand(args: string[], logger: logging.Logger): Promise<number> {
Expand Down Expand Up @@ -111,7 +80,7 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
};

let localYargs = yargs(args);
for (const CommandModule of COMMANDS) {
for (const CommandModule of await getCommandsToRegister(positional[0])) {
localYargs = addCommandModuleToYargs(localYargs, CommandModule, context);
}

Expand Down Expand Up @@ -168,3 +137,23 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis

return process.exitCode ?? 0;
}

/**
* Get the commands that need to be registered.
* @returns One or more command factories that needs to be registered.
*/
async function getCommandsToRegister(
commandName: string | number,
): Promise<CommandModuleConstructor[]> {
const commands: CommandConfig[] = [];
if (commandName in RootCommands) {
commands.push(RootCommands[commandName as CommandNames]);
} else if (commandName in RootCommandsAliases) {
commands.push(RootCommandsAliases[commandName]);
} else {
// Unknown command, register every possible command.
Object.values(RootCommands).forEach((c) => commands.push(c));
}

return Promise.all(commands.map((command) => command.factory().then((m) => m.default)));
}
14 changes: 8 additions & 6 deletions packages/angular/cli/src/command-builder/utilities/command.ts
Expand Up @@ -16,13 +16,15 @@ import {
} from '../command-module';

export const demandCommandFailureMessage = `You need to specify a command before moving on. Use '--help' to view the available commands.`;
export type CommandModuleConstructor = Partial<CommandModuleImplementation> & {
new (context: CommandContext): Partial<CommandModuleImplementation> & CommandModule;
};

export function addCommandModuleToYargs<
T extends object,
U extends Partial<CommandModuleImplementation> & {
new (context: CommandContext): Partial<CommandModuleImplementation> & CommandModule;
},
>(localYargs: Argv<T>, commandModule: U, context: CommandContext): Argv<T> {
export function addCommandModuleToYargs<T extends object, U extends CommandModuleConstructor>(
localYargs: Argv<T>,
commandModule: U,
context: CommandContext,
): Argv<T> {
const cmd = new commandModule(context);
const {
args: {
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/cli/src/commands/add/cli.ts
Expand Up @@ -55,7 +55,7 @@ const packageVersionExclusions: Record<string, string | Range> = {
'@angular/material': '7.x',
};

export class AddCommandModule
export default class AddCommadModule
extends SchematicsCommandModule
implements CommandModuleImplementation<AddCommandArgs>
{
Expand Down
5 changes: 4 additions & 1 deletion packages/angular/cli/src/commands/analytics/cli.ts
Expand Up @@ -24,7 +24,10 @@ import {
AnalyticsPromptModule,
} from './settings/cli';

export class AnalyticsCommandModule extends CommandModule implements CommandModuleImplementation {
export default class AnalyticsCommandModule
extends CommandModule
implements CommandModuleImplementation
{
command = 'analytics';
describe = 'Configures the gathering of Angular CLI usage metrics.';
longDescriptionPath = join(__dirname, 'long-description.md');
Expand Down
5 changes: 3 additions & 2 deletions packages/angular/cli/src/commands/build/cli.ts
Expand Up @@ -9,14 +9,15 @@
import { join } from 'path';
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
import { CommandModuleImplementation } from '../../command-builder/command-module';
import { RootCommands } from '../command-config';

export class BuildCommandModule
export default class BuildCommandModule
extends ArchitectCommandModule
implements CommandModuleImplementation
{
multiTarget = false;
command = 'build [project]';
aliases = ['b'];
aliases = RootCommands['build'].aliases;
describe =
'Compiles an Angular application or library into an output directory named dist/ at the given output path.';
longDescriptionPath = join(__dirname, 'long-description.md');
Expand Down
5 changes: 4 additions & 1 deletion packages/angular/cli/src/commands/cache/cli.ts
Expand Up @@ -22,7 +22,10 @@ import { CacheCleanModule } from './clean/cli';
import { CacheInfoCommandModule } from './info/cli';
import { CacheDisableModule, CacheEnableModule } from './settings/cli';

export class CacheCommandModule extends CommandModule implements CommandModuleImplementation {
export default class CacheCommandModule
extends CommandModule
implements CommandModuleImplementation
{
command = 'cache';
describe = 'Configure persistent disk cache and retrieve cache statistics.';
longDescriptionPath = join(__dirname, 'long-description.md');
Expand Down
114 changes: 114 additions & 0 deletions packages/angular/cli/src/commands/command-config.ts
@@ -0,0 +1,114 @@
/**
* @license
* Copyright Google LLC 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 { CommandModuleConstructor } from '../command-builder/utilities/command';

export type CommandNames =
| 'add'
| 'analytics'
| 'build'
| 'cache'
| 'completion'
| 'config'
| 'deploy'
| 'doc'
| 'e2e'
| 'extract-i18n'
| 'generate'
| 'lint'
| 'make-this-awesome'
| 'new'
| 'run'
| 'serve'
| 'test'
| 'update'
| 'version';

export interface CommandConfig {
aliases?: string[];
factory: () => Promise<{ default: CommandModuleConstructor }>;
}

export const RootCommands: Record<
/* Command */ CommandNames & string,
/* Command Config */ CommandConfig
> = {
'add': {
factory: () => import('./add/cli'),
},
'analytics': {
factory: () => import('./analytics/cli'),
},
'build': {
factory: () => import('./build/cli'),
aliases: ['b'],
},
'cache': {
factory: () => import('./cache/cli'),
},
'completion': {
factory: () => import('./completion/cli'),
},
'config': {
factory: () => import('./config/cli'),
},
'deploy': {
factory: () => import('./deploy/cli'),
},
'doc': {
factory: () => import('./doc/cli'),
aliases: ['d'],
},
'e2e': {
factory: () => import('./e2e/cli'),
aliases: ['e2e'],
},
'extract-i18n': {
factory: () => import('./extract-i18n/cli'),
},
'generate': {
factory: () => import('./generate/cli'),
aliases: ['g'],
},
'lint': {
factory: () => import('./lint/cli'),
},
'make-this-awesome': {
factory: () => import('./make-this-awesome/cli'),
},
'new': {
factory: () => import('./new/cli'),
aliases: ['n'],
},
'run': {
factory: () => import('./run/cli'),
},
'serve': {
factory: () => import('./serve/cli'),
aliases: ['s'],
},
'test': {
factory: () => import('./test/cli'),
aliases: ['t'],
},
'update': {
factory: () => import('./update/cli'),
},
'version': {
factory: () => import('./version/cli'),
aliases: ['v'],
},
};

export const RootCommandsAliases = Object.values(RootCommands).reduce((prev, current) => {
current.aliases?.forEach((alias) => {
prev[alias] = current;
});

return prev;
}, {} as Record<string, CommandConfig>);
5 changes: 4 additions & 1 deletion packages/angular/cli/src/commands/completion/cli.ts
Expand Up @@ -14,7 +14,10 @@ import { colors } from '../../utilities/color';
import { hasGlobalCliInstall, initializeAutocomplete } from '../../utilities/completion';
import { assertIsError } from '../../utilities/error';

export class CompletionCommandModule extends CommandModule implements CommandModuleImplementation {
export default class CompletionCommandModule
extends CommandModule
implements CommandModuleImplementation
{
command = 'completion';
describe = 'Set up Angular CLI autocompletion for your terminal.';
longDescriptionPath = join(__dirname, 'long-description.md');
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/cli/src/commands/config/cli.ts
Expand Up @@ -25,7 +25,7 @@ interface ConfigCommandArgs {
global?: boolean;
}

export class ConfigCommandModule
export default class ConfigCommandModule
extends CommandModule<ConfigCommandArgs>
implements CommandModuleImplementation<ConfigCommandArgs>
{
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/cli/src/commands/deploy/cli.ts
Expand Up @@ -11,7 +11,7 @@ import { MissingTargetChoice } from '../../command-builder/architect-base-comman
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
import { CommandModuleImplementation } from '../../command-builder/command-module';

export class DeployCommandModule
export default class DeployCommandModule
extends ArchitectCommandModule
implements CommandModuleImplementation
{
Expand Down
5 changes: 3 additions & 2 deletions packages/angular/cli/src/commands/doc/cli.ts
Expand Up @@ -13,19 +13,20 @@ import {
CommandModuleImplementation,
Options,
} from '../../command-builder/command-module';
import { RootCommands } from '../command-config';

interface DocCommandArgs {
keyword: string;
search?: boolean;
version?: string;
}

export class DocCommandModule
export default class DocCommandModule
extends CommandModule<DocCommandArgs>
implements CommandModuleImplementation<DocCommandArgs>
{
command = 'doc <keyword>';
aliases = ['d'];
aliases = RootCommands['doc'].aliases;
describe =
'Opens the official Angular documentation (angular.io) in a browser, and searches for a given keyword.';
longDescriptionPath?: string;
Expand Down
5 changes: 3 additions & 2 deletions packages/angular/cli/src/commands/e2e/cli.ts
Expand Up @@ -9,8 +9,9 @@
import { MissingTargetChoice } from '../../command-builder/architect-base-command-module';
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
import { CommandModuleImplementation } from '../../command-builder/command-module';
import { RootCommands } from '../command-config';

export class E2eCommandModule
export default class E2eCommandModule
extends ArchitectCommandModule
implements CommandModuleImplementation
{
Expand All @@ -31,7 +32,7 @@ export class E2eCommandModule

multiTarget = true;
command = 'e2e [project]';
aliases = ['e'];
aliases = RootCommands['e2e'].aliases;
describe = 'Builds and serves an Angular application, then runs end-to-end tests.';
longDescriptionPath?: string;
}
2 changes: 1 addition & 1 deletion packages/angular/cli/src/commands/extract-i18n/cli.ts
Expand Up @@ -9,7 +9,7 @@
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
import { CommandModuleImplementation } from '../../command-builder/command-module';

export class ExtractI18nCommandModule
export default class ExtractI18nCommandModule
extends ArchitectCommandModule
implements CommandModuleImplementation
{
Expand Down
5 changes: 3 additions & 2 deletions packages/angular/cli/src/commands/generate/cli.ts
Expand Up @@ -25,17 +25,18 @@ import {
} from '../../command-builder/schematics-command-module';
import { demandCommandFailureMessage } from '../../command-builder/utilities/command';
import { Option } from '../../command-builder/utilities/json-schema';
import { RootCommands } from '../command-config';

interface GenerateCommandArgs extends SchematicsCommandArgs {
schematic?: string;
}

export class GenerateCommandModule
export default class GenerateCommandModule
extends SchematicsCommandModule
implements CommandModuleImplementation<GenerateCommandArgs>
{
command = 'generate';
aliases = 'g';
aliases = RootCommands['generate'].aliases;
describe = 'Generates and/or modifies files based on a schematic.';
longDescriptionPath?: string | undefined;

Expand Down

0 comments on commit f9b2fb1

Please sign in to comment.