Skip to content

Commit f9b2fb1

Browse files
alan-agius4dgp1130
authored andcommitted
perf(@angular/cli): register CLI commands lazily
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)
1 parent 4d81cb4 commit f9b2fb1

File tree

22 files changed

+204
-76
lines changed

22 files changed

+204
-76
lines changed

packages/angular/cli/src/command-builder/command-runner.ts

+32-43
Original file line numberDiff line numberDiff line change
@@ -9,56 +9,25 @@
99
import { logging } from '@angular-devkit/core';
1010
import yargs from 'yargs';
1111
import { Parser } from 'yargs/helpers';
12-
import { AddCommandModule } from '../commands/add/cli';
13-
import { AnalyticsCommandModule } from '../commands/analytics/cli';
14-
import { BuildCommandModule } from '../commands/build/cli';
15-
import { CacheCommandModule } from '../commands/cache/cli';
16-
import { CompletionCommandModule } from '../commands/completion/cli';
17-
import { ConfigCommandModule } from '../commands/config/cli';
18-
import { DeployCommandModule } from '../commands/deploy/cli';
19-
import { DocCommandModule } from '../commands/doc/cli';
20-
import { E2eCommandModule } from '../commands/e2e/cli';
21-
import { ExtractI18nCommandModule } from '../commands/extract-i18n/cli';
22-
import { GenerateCommandModule } from '../commands/generate/cli';
23-
import { LintCommandModule } from '../commands/lint/cli';
24-
import { AwesomeCommandModule } from '../commands/make-this-awesome/cli';
25-
import { NewCommandModule } from '../commands/new/cli';
26-
import { RunCommandModule } from '../commands/run/cli';
27-
import { ServeCommandModule } from '../commands/serve/cli';
28-
import { TestCommandModule } from '../commands/test/cli';
29-
import { UpdateCommandModule } from '../commands/update/cli';
30-
import { VersionCommandModule } from '../commands/version/cli';
12+
import {
13+
CommandConfig,
14+
CommandNames,
15+
RootCommands,
16+
RootCommandsAliases,
17+
} from '../commands/command-config';
3118
import { colors } from '../utilities/color';
3219
import { AngularWorkspace, getWorkspace } from '../utilities/config';
3320
import { assertIsError } from '../utilities/error';
3421
import { PackageManagerUtils } from '../utilities/package-manager';
3522
import { CommandContext, CommandModuleError } from './command-module';
36-
import { addCommandModuleToYargs, demandCommandFailureMessage } from './utilities/command';
23+
import {
24+
CommandModuleConstructor,
25+
addCommandModuleToYargs,
26+
demandCommandFailureMessage,
27+
} from './utilities/command';
3728
import { jsonHelpUsage } from './utilities/json-help';
3829
import { normalizeOptionsMiddleware } from './utilities/normalize-options-middleware';
3930

40-
const COMMANDS = [
41-
VersionCommandModule,
42-
DocCommandModule,
43-
AwesomeCommandModule,
44-
ConfigCommandModule,
45-
AnalyticsCommandModule,
46-
AddCommandModule,
47-
GenerateCommandModule,
48-
BuildCommandModule,
49-
E2eCommandModule,
50-
TestCommandModule,
51-
ServeCommandModule,
52-
ExtractI18nCommandModule,
53-
DeployCommandModule,
54-
LintCommandModule,
55-
NewCommandModule,
56-
UpdateCommandModule,
57-
RunCommandModule,
58-
CacheCommandModule,
59-
CompletionCommandModule,
60-
].sort(); // Will be sorted by class name.
61-
6231
const yargsParser = Parser as unknown as typeof Parser.default;
6332

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

11382
let localYargs = yargs(args);
114-
for (const CommandModule of COMMANDS) {
83+
for (const CommandModule of await getCommandsToRegister(positional[0])) {
11584
localYargs = addCommandModuleToYargs(localYargs, CommandModule, context);
11685
}
11786

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

169138
return process.exitCode ?? 0;
170139
}
140+
141+
/**
142+
* Get the commands that need to be registered.
143+
* @returns One or more command factories that needs to be registered.
144+
*/
145+
async function getCommandsToRegister(
146+
commandName: string | number,
147+
): Promise<CommandModuleConstructor[]> {
148+
const commands: CommandConfig[] = [];
149+
if (commandName in RootCommands) {
150+
commands.push(RootCommands[commandName as CommandNames]);
151+
} else if (commandName in RootCommandsAliases) {
152+
commands.push(RootCommandsAliases[commandName]);
153+
} else {
154+
// Unknown command, register every possible command.
155+
Object.values(RootCommands).forEach((c) => commands.push(c));
156+
}
157+
158+
return Promise.all(commands.map((command) => command.factory().then((m) => m.default)));
159+
}

packages/angular/cli/src/command-builder/utilities/command.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ import {
1616
} from '../command-module';
1717

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

20-
export function addCommandModuleToYargs<
21-
T extends object,
22-
U extends Partial<CommandModuleImplementation> & {
23-
new (context: CommandContext): Partial<CommandModuleImplementation> & CommandModule;
24-
},
25-
>(localYargs: Argv<T>, commandModule: U, context: CommandContext): Argv<T> {
23+
export function addCommandModuleToYargs<T extends object, U extends CommandModuleConstructor>(
24+
localYargs: Argv<T>,
25+
commandModule: U,
26+
context: CommandContext,
27+
): Argv<T> {
2628
const cmd = new commandModule(context);
2729
const {
2830
args: {

packages/angular/cli/src/commands/add/cli.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const packageVersionExclusions: Record<string, string | Range> = {
5555
'@angular/material': '7.x',
5656
};
5757

58-
export class AddCommandModule
58+
export default class AddCommadModule
5959
extends SchematicsCommandModule
6060
implements CommandModuleImplementation<AddCommandArgs>
6161
{

packages/angular/cli/src/commands/analytics/cli.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import {
2424
AnalyticsPromptModule,
2525
} from './settings/cli';
2626

27-
export class AnalyticsCommandModule extends CommandModule implements CommandModuleImplementation {
27+
export default class AnalyticsCommandModule
28+
extends CommandModule
29+
implements CommandModuleImplementation
30+
{
2831
command = 'analytics';
2932
describe = 'Configures the gathering of Angular CLI usage metrics.';
3033
longDescriptionPath = join(__dirname, 'long-description.md');

packages/angular/cli/src/commands/build/cli.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
import { join } from 'path';
1010
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
1111
import { CommandModuleImplementation } from '../../command-builder/command-module';
12+
import { RootCommands } from '../command-config';
1213

13-
export class BuildCommandModule
14+
export default class BuildCommandModule
1415
extends ArchitectCommandModule
1516
implements CommandModuleImplementation
1617
{
1718
multiTarget = false;
1819
command = 'build [project]';
19-
aliases = ['b'];
20+
aliases = RootCommands['build'].aliases;
2021
describe =
2122
'Compiles an Angular application or library into an output directory named dist/ at the given output path.';
2223
longDescriptionPath = join(__dirname, 'long-description.md');

packages/angular/cli/src/commands/cache/cli.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import { CacheCleanModule } from './clean/cli';
2222
import { CacheInfoCommandModule } from './info/cli';
2323
import { CacheDisableModule, CacheEnableModule } from './settings/cli';
2424

25-
export class CacheCommandModule extends CommandModule implements CommandModuleImplementation {
25+
export default class CacheCommandModule
26+
extends CommandModule
27+
implements CommandModuleImplementation
28+
{
2629
command = 'cache';
2730
describe = 'Configure persistent disk cache and retrieve cache statistics.';
2831
longDescriptionPath = join(__dirname, 'long-description.md');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { CommandModuleConstructor } from '../command-builder/utilities/command';
10+
11+
export type CommandNames =
12+
| 'add'
13+
| 'analytics'
14+
| 'build'
15+
| 'cache'
16+
| 'completion'
17+
| 'config'
18+
| 'deploy'
19+
| 'doc'
20+
| 'e2e'
21+
| 'extract-i18n'
22+
| 'generate'
23+
| 'lint'
24+
| 'make-this-awesome'
25+
| 'new'
26+
| 'run'
27+
| 'serve'
28+
| 'test'
29+
| 'update'
30+
| 'version';
31+
32+
export interface CommandConfig {
33+
aliases?: string[];
34+
factory: () => Promise<{ default: CommandModuleConstructor }>;
35+
}
36+
37+
export const RootCommands: Record<
38+
/* Command */ CommandNames & string,
39+
/* Command Config */ CommandConfig
40+
> = {
41+
'add': {
42+
factory: () => import('./add/cli'),
43+
},
44+
'analytics': {
45+
factory: () => import('./analytics/cli'),
46+
},
47+
'build': {
48+
factory: () => import('./build/cli'),
49+
aliases: ['b'],
50+
},
51+
'cache': {
52+
factory: () => import('./cache/cli'),
53+
},
54+
'completion': {
55+
factory: () => import('./completion/cli'),
56+
},
57+
'config': {
58+
factory: () => import('./config/cli'),
59+
},
60+
'deploy': {
61+
factory: () => import('./deploy/cli'),
62+
},
63+
'doc': {
64+
factory: () => import('./doc/cli'),
65+
aliases: ['d'],
66+
},
67+
'e2e': {
68+
factory: () => import('./e2e/cli'),
69+
aliases: ['e2e'],
70+
},
71+
'extract-i18n': {
72+
factory: () => import('./extract-i18n/cli'),
73+
},
74+
'generate': {
75+
factory: () => import('./generate/cli'),
76+
aliases: ['g'],
77+
},
78+
'lint': {
79+
factory: () => import('./lint/cli'),
80+
},
81+
'make-this-awesome': {
82+
factory: () => import('./make-this-awesome/cli'),
83+
},
84+
'new': {
85+
factory: () => import('./new/cli'),
86+
aliases: ['n'],
87+
},
88+
'run': {
89+
factory: () => import('./run/cli'),
90+
},
91+
'serve': {
92+
factory: () => import('./serve/cli'),
93+
aliases: ['s'],
94+
},
95+
'test': {
96+
factory: () => import('./test/cli'),
97+
aliases: ['t'],
98+
},
99+
'update': {
100+
factory: () => import('./update/cli'),
101+
},
102+
'version': {
103+
factory: () => import('./version/cli'),
104+
aliases: ['v'],
105+
},
106+
};
107+
108+
export const RootCommandsAliases = Object.values(RootCommands).reduce((prev, current) => {
109+
current.aliases?.forEach((alias) => {
110+
prev[alias] = current;
111+
});
112+
113+
return prev;
114+
}, {} as Record<string, CommandConfig>);

packages/angular/cli/src/commands/completion/cli.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import { colors } from '../../utilities/color';
1414
import { hasGlobalCliInstall, initializeAutocomplete } from '../../utilities/completion';
1515
import { assertIsError } from '../../utilities/error';
1616

17-
export class CompletionCommandModule extends CommandModule implements CommandModuleImplementation {
17+
export default class CompletionCommandModule
18+
extends CommandModule
19+
implements CommandModuleImplementation
20+
{
1821
command = 'completion';
1922
describe = 'Set up Angular CLI autocompletion for your terminal.';
2023
longDescriptionPath = join(__dirname, 'long-description.md');

packages/angular/cli/src/commands/config/cli.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ interface ConfigCommandArgs {
2525
global?: boolean;
2626
}
2727

28-
export class ConfigCommandModule
28+
export default class ConfigCommandModule
2929
extends CommandModule<ConfigCommandArgs>
3030
implements CommandModuleImplementation<ConfigCommandArgs>
3131
{

packages/angular/cli/src/commands/deploy/cli.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { MissingTargetChoice } from '../../command-builder/architect-base-comman
1111
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
1212
import { CommandModuleImplementation } from '../../command-builder/command-module';
1313

14-
export class DeployCommandModule
14+
export default class DeployCommandModule
1515
extends ArchitectCommandModule
1616
implements CommandModuleImplementation
1717
{

packages/angular/cli/src/commands/doc/cli.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,20 @@ import {
1313
CommandModuleImplementation,
1414
Options,
1515
} from '../../command-builder/command-module';
16+
import { RootCommands } from '../command-config';
1617

1718
interface DocCommandArgs {
1819
keyword: string;
1920
search?: boolean;
2021
version?: string;
2122
}
2223

23-
export class DocCommandModule
24+
export default class DocCommandModule
2425
extends CommandModule<DocCommandArgs>
2526
implements CommandModuleImplementation<DocCommandArgs>
2627
{
2728
command = 'doc <keyword>';
28-
aliases = ['d'];
29+
aliases = RootCommands['doc'].aliases;
2930
describe =
3031
'Opens the official Angular documentation (angular.io) in a browser, and searches for a given keyword.';
3132
longDescriptionPath?: string;

packages/angular/cli/src/commands/e2e/cli.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
import { MissingTargetChoice } from '../../command-builder/architect-base-command-module';
1010
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
1111
import { CommandModuleImplementation } from '../../command-builder/command-module';
12+
import { RootCommands } from '../command-config';
1213

13-
export class E2eCommandModule
14+
export default class E2eCommandModule
1415
extends ArchitectCommandModule
1516
implements CommandModuleImplementation
1617
{
@@ -31,7 +32,7 @@ export class E2eCommandModule
3132

3233
multiTarget = true;
3334
command = 'e2e [project]';
34-
aliases = ['e'];
35+
aliases = RootCommands['e2e'].aliases;
3536
describe = 'Builds and serves an Angular application, then runs end-to-end tests.';
3637
longDescriptionPath?: string;
3738
}

packages/angular/cli/src/commands/extract-i18n/cli.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import { ArchitectCommandModule } from '../../command-builder/architect-command-module';
1010
import { CommandModuleImplementation } from '../../command-builder/command-module';
1111

12-
export class ExtractI18nCommandModule
12+
export default class ExtractI18nCommandModule
1313
extends ArchitectCommandModule
1414
implements CommandModuleImplementation
1515
{

packages/angular/cli/src/commands/generate/cli.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,18 @@ import {
2525
} from '../../command-builder/schematics-command-module';
2626
import { demandCommandFailureMessage } from '../../command-builder/utilities/command';
2727
import { Option } from '../../command-builder/utilities/json-schema';
28+
import { RootCommands } from '../command-config';
2829

2930
interface GenerateCommandArgs extends SchematicsCommandArgs {
3031
schematic?: string;
3132
}
3233

33-
export class GenerateCommandModule
34+
export default class GenerateCommandModule
3435
extends SchematicsCommandModule
3536
implements CommandModuleImplementation<GenerateCommandArgs>
3637
{
3738
command = 'generate';
38-
aliases = 'g';
39+
aliases = RootCommands['generate'].aliases;
3940
describe = 'Generates and/or modifies files based on a schematic.';
4041
longDescriptionPath?: string | undefined;
4142

0 commit comments

Comments
 (0)