Skip to content

Commit 19f9d35

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 a710a26 commit 19f9d35

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

Lines changed: 32 additions & 43 deletions
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

Lines changed: 8 additions & 6 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 4 additions & 1 deletion
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

Lines changed: 3 additions & 2 deletions
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

Lines changed: 4 additions & 1 deletion
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');
Lines changed: 114 additions & 0 deletions
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

Lines changed: 4 additions & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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
{

0 commit comments

Comments
 (0)