Skip to content

Commit 1bbabb2

Browse files
committed
refactor(@angular/cli): create package manager util class
Apart from better code quality, this helps reduce the time of CLI bootstrapping time, as retrieving the package manager name is a rather expensive operator due to the number of process spawns. The package manager name isn't always needed until we run a command and therefore in some cases we can see an improvement of around `~600ms`. Ex: `ng b --help`. From ` 1.34s` to `0.76s`. This will be important when we eventually introduce auto complete as users will get faster loopback.
1 parent 746d0c5 commit 1bbabb2

File tree

10 files changed

+380
-406
lines changed

10 files changed

+380
-406
lines changed

Diff for: packages/angular/cli/src/command-builder/command-module.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import {
1818
Options as YargsOptions,
1919
} from 'yargs';
2020
import { Parser as yargsParser } from 'yargs/helpers';
21-
import { PackageManager } from '../../lib/config/workspace-schema';
2221
import { createAnalytics } from '../analytics/analytics';
2322
import { AngularWorkspace } from '../utilities/config';
23+
import { PackageManagerUtils } from '../utilities/package-manager';
2424
import { Option } from './utilities/json-schema';
2525

2626
export type Options<T> = { [key in keyof T as CamelCaseKey<key>]: T[key] };
@@ -40,7 +40,7 @@ export interface CommandContext {
4040
workspace?: AngularWorkspace;
4141
globalConfiguration?: AngularWorkspace;
4242
logger: logging.Logger;
43-
packageManager: PackageManager;
43+
packageManager: PackageManagerUtils;
4444
/** Arguments parsed in free-from without parser configuration. */
4545
args: {
4646
positional: string[];

Diff for: packages/angular/cli/src/command-builder/command-runner.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { UpdateCommandModule } from '../commands/update/cli';
2929
import { VersionCommandModule } from '../commands/version/cli';
3030
import { colors } from '../utilities/color';
3131
import { AngularWorkspace, getWorkspace } from '../utilities/config';
32-
import { getPackageManager } from '../utilities/package-manager';
32+
import { PackageManagerUtils } from '../utilities/package-manager';
3333
import { CommandContext, CommandModuleError, CommandScope } from './command-module';
3434
import { addCommandModuleToYargs, demandCommandFailureMessage } from './utilities/command';
3535
import { jsonHelpUsage } from './utilities/json-help';
@@ -80,14 +80,13 @@ export async function runCommand(args: string[], logger: logging.Logger): Promis
8080
}
8181

8282
const root = workspace?.basePath ?? process.cwd();
83-
8483
const context: CommandContext = {
8584
globalConfiguration,
8685
workspace,
8786
logger,
8887
currentDirectory: process.cwd(),
89-
root: workspace?.basePath ?? process.cwd(),
90-
packageManager: await getPackageManager(workspace?.basePath ?? process.cwd()),
88+
root,
89+
packageManager: new PackageManagerUtils({ globalConfiguration, workspace, root }),
9190
args: {
9291
positional: positional.map((v) => v.toString()),
9392
options: {

Diff for: packages/angular/cli/src/command-builder/schematics-command-module.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export abstract class SchematicsCommandModule
122122
const workflow = new NodeWorkflow(root, {
123123
force,
124124
dryRun,
125-
packageManager,
125+
packageManager: packageManager.name,
126126
// A schema registry is required to allow customizing addUndefinedDefaults
127127
registry: new schema.CoreSchemaRegistry(formats.standardFormats),
128128
packageRegistry,

Diff for: packages/angular/cli/src/commands/add/cli.ts

+10-14
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ import {
2424
SchematicsCommandModule,
2525
} from '../../command-builder/schematics-command-module';
2626
import { colors } from '../../utilities/color';
27-
import { installPackage, installTempPackage } from '../../utilities/install-package';
28-
import { ensureCompatibleNpm } from '../../utilities/package-manager';
2927
import {
3028
NgAddSaveDependency,
3129
PackageManifest,
@@ -107,9 +105,9 @@ export class AddCommandModule
107105

108106
// eslint-disable-next-line max-lines-per-function
109107
async run(options: Options<AddCommandArgs> & OtherOptions): Promise<number | void> {
110-
const { root, logger, packageManager } = this.context;
108+
const { logger, packageManager } = this.context;
111109
const { verbose, registry, collection, skipConfirmation } = options;
112-
await ensureCompatibleNpm(root);
110+
packageManager.ensureCompatibility();
113111

114112
let packageIdentifier;
115113
try {
@@ -137,8 +135,8 @@ export class AddCommandModule
137135
const spinner = new Spinner();
138136

139137
spinner.start('Determining package manager...');
140-
const usingYarn = packageManager === PackageManager.Yarn;
141-
spinner.info(`Using package manager: ${colors.grey(packageManager)}`);
138+
const usingYarn = packageManager.name === PackageManager.Yarn;
139+
spinner.info(`Using package manager: ${colors.grey(packageManager.name)}`);
142140

143141
if (packageIdentifier.name && packageIdentifier.type === 'tag' && !packageIdentifier.rawSpec) {
144142
// only package name provided; search for viable version
@@ -270,30 +268,28 @@ export class AddCommandModule
270268
if (savePackage === false) {
271269
// Temporary packages are located in a different directory
272270
// Hence we need to resolve them using the temp path
273-
const { status, tempNodeModules } = await installTempPackage(
271+
const { success, tempNodeModules } = await packageManager.installTemp(
274272
packageIdentifier.raw,
275-
packageManager,
276273
registry ? [`--registry="${registry}"`] : undefined,
277274
);
278275
const resolvedCollectionPath = require.resolve(join(collectionName, 'package.json'), {
279276
paths: [tempNodeModules],
280277
});
281278

282-
if (status !== 0) {
283-
return status;
279+
if (!success) {
280+
return 1;
284281
}
285282

286283
collectionName = dirname(resolvedCollectionPath);
287284
} else {
288-
const status = await installPackage(
285+
const success = await packageManager.install(
289286
packageIdentifier.raw,
290-
packageManager,
291287
savePackage,
292288
registry ? [`--registry="${registry}"`] : undefined,
293289
);
294290

295-
if (status !== 0) {
296-
return status;
291+
if (!success) {
292+
return 1;
297293
}
298294
}
299295

Diff for: packages/angular/cli/src/commands/new/cli.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
SchematicsCommandArgs,
1919
SchematicsCommandModule,
2020
} from '../../command-builder/schematics-command-module';
21-
import { ensureCompatibleNpm } from '../../utilities/package-manager';
2221
import { VERSION } from '../../utilities/version';
2322

2423
interface NewCommandArgs extends SchematicsCommandArgs {
@@ -75,7 +74,7 @@ export class NewCommandModule
7574
!schematicOptions.skipInstall &&
7675
(schematicOptions.packageManager === undefined || schematicOptions.packageManager === 'npm')
7776
) {
78-
await ensureCompatibleNpm(this.context.root);
77+
this.context.packageManager.ensureCompatibility();
7978
}
8079

8180
return this.runSchematic({

Diff for: packages/angular/cli/src/commands/update/cli.ts

+59-20
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88

99
import { UnsuccessfulWorkflowExecution } from '@angular-devkit/schematics';
1010
import { NodeWorkflow } from '@angular-devkit/schematics/tools';
11-
import { execSync } from 'child_process';
12-
import { existsSync, promises as fsPromises } from 'fs';
11+
import { execSync, spawnSync } from 'child_process';
12+
import { existsSync, promises as fs } from 'fs';
1313
import npa from 'npm-package-arg';
1414
import pickManifest from 'npm-pick-manifest';
1515
import * as path from 'path';
16-
import { join } from 'path';
16+
import { join, resolve } from 'path';
1717
import * as semver from 'semver';
1818
import { Argv } from 'yargs';
1919
import { PackageManager } from '../../../lib/config/workspace-schema';
@@ -27,9 +27,7 @@ import { SchematicEngineHost } from '../../command-builder/utilities/schematic-e
2727
import { subscribeToWorkflow } from '../../command-builder/utilities/schematic-workflow';
2828
import { colors } from '../../utilities/color';
2929
import { disableVersionCheck } from '../../utilities/environment-options';
30-
import { installAllPackages, runTempPackageBin } from '../../utilities/install-package';
3130
import { writeErrorToLogFile } from '../../utilities/log-file';
32-
import { ensureCompatibleNpm } from '../../utilities/package-manager';
3331
import {
3432
PackageIdentifier,
3533
PackageManifest,
@@ -167,7 +165,7 @@ export class UpdateCommandModule extends CommandModule<UpdateCommandArgs> {
167165
async run(options: Options<UpdateCommandArgs>): Promise<number | void> {
168166
const { logger, packageManager } = this.context;
169167

170-
await ensureCompatibleNpm(this.context.root);
168+
packageManager.ensureCompatibility();
171169

172170
// Check if the current installed CLI version is older than the latest compatible version.
173171
if (!disableVersionCheck) {
@@ -183,11 +181,7 @@ export class UpdateCommandModule extends CommandModule<UpdateCommandArgs> {
183181
`Installing a temporary Angular CLI versioned ${cliVersionToInstall} to perform the update.`,
184182
);
185183

186-
return runTempPackageBin(
187-
`@angular/cli@${cliVersionToInstall}`,
188-
packageManager,
189-
process.argv.slice(2),
190-
);
184+
return this.runTempBinary(`@angular/cli@${cliVersionToInstall}`, process.argv.slice(2));
191185
}
192186
}
193187

@@ -233,7 +227,7 @@ export class UpdateCommandModule extends CommandModule<UpdateCommandArgs> {
233227
logger.info(`Found ${rootDependencies.size} dependencies.`);
234228

235229
const workflow = new NodeWorkflow(this.context.root, {
236-
packageManager: this.context.packageManager,
230+
packageManager: packageManager.name,
237231
packageManagerForce: options.force,
238232
// __dirname -> favor @schematics/update from this package
239233
// Otherwise, use packages from the active workspace (migrations)
@@ -252,7 +246,7 @@ export class UpdateCommandModule extends CommandModule<UpdateCommandArgs> {
252246
force: options.force,
253247
next: options.next,
254248
verbose: options.verbose,
255-
packageManager,
249+
packageManager: packageManager.name,
256250
packages: [],
257251
},
258252
);
@@ -681,28 +675,27 @@ export class UpdateCommandModule extends CommandModule<UpdateCommandArgs> {
681675
verbose: options.verbose,
682676
force: options.force,
683677
next: options.next,
684-
packageManager: this.context.packageManager,
678+
packageManager: this.context.packageManager.name,
685679
packages: packagesToUpdate,
686680
},
687681
);
688682

689683
if (success) {
690684
try {
691-
await fsPromises.rm(path.join(this.context.root, 'node_modules'), {
685+
await fs.rm(path.join(this.context.root, 'node_modules'), {
692686
force: true,
693687
recursive: true,
694688
maxRetries: 3,
695689
});
696690
} catch {}
697691

698-
const result = await installAllPackages(
699-
this.context.packageManager,
692+
const installationSuccess = await this.context.packageManager.installAll(
700693
options.force ? ['--force'] : [],
701694
this.context.root,
702695
);
703696

704-
if (result !== 0) {
705-
return result;
697+
if (!installationSuccess) {
698+
return 1;
706699
}
707700
}
708701

@@ -891,7 +884,7 @@ export class UpdateCommandModule extends CommandModule<UpdateCommandArgs> {
891884
this.context.logger,
892885
{
893886
verbose,
894-
usingYarn: this.context.packageManager === PackageManager.Yarn,
887+
usingYarn: this.context.packageManager.name === PackageManager.Yarn,
895888
},
896889
);
897890

@@ -928,6 +921,52 @@ export class UpdateCommandModule extends CommandModule<UpdateCommandArgs> {
928921
// We end up using Angular ClI v13 to run the migrations if we run the migrations using the CLI installed major version + 1 logic.
929922
return VERSION.major;
930923
}
924+
925+
private async runTempBinary(packageName: string, args: string[] = []): Promise<number> {
926+
const { success, tempNodeModules } = await this.context.packageManager.installTemp(packageName);
927+
if (!success) {
928+
return 1;
929+
}
930+
931+
// Remove version/tag etc... from package name
932+
// Ex: @angular/cli@latest -> @angular/cli
933+
const packageNameNoVersion = packageName.substring(0, packageName.lastIndexOf('@'));
934+
const pkgLocation = join(tempNodeModules, packageNameNoVersion);
935+
const packageJsonPath = join(pkgLocation, 'package.json');
936+
937+
// Get a binary location for this package
938+
let binPath: string | undefined;
939+
if (existsSync(packageJsonPath)) {
940+
const content = await fs.readFile(packageJsonPath, 'utf-8');
941+
if (content) {
942+
const { bin = {} } = JSON.parse(content);
943+
const binKeys = Object.keys(bin);
944+
945+
if (binKeys.length) {
946+
binPath = resolve(pkgLocation, bin[binKeys[0]]);
947+
}
948+
}
949+
}
950+
951+
if (!binPath) {
952+
throw new Error(`Cannot locate bin for temporary package: ${packageNameNoVersion}.`);
953+
}
954+
955+
const { status, error } = spawnSync(process.execPath, [binPath, ...args], {
956+
stdio: 'inherit',
957+
env: {
958+
...process.env,
959+
NG_DISABLE_VERSION_CHECK: 'true',
960+
NG_CLI_ANALYTICS: 'false',
961+
},
962+
});
963+
964+
if (status === null && error) {
965+
throw error;
966+
}
967+
968+
return status ?? 0;
969+
}
931970
}
932971

933972
/**

Diff for: packages/angular/cli/src/commands/version/cli.ts

+3-24
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import { execSync } from 'child_process';
109
import nodeModule from 'module';
1110
import { resolve } from 'path';
1211
import { Argv } from 'yargs';
@@ -49,10 +48,10 @@ export class VersionCommandModule extends CommandModule implements CommandModule
4948
}
5049

5150
async run(): Promise<void> {
52-
const logger = this.context.logger;
51+
const { packageManager, logger, root } = this.context;
5352
const localRequire = nodeModule.createRequire(resolve(__filename, '../../../'));
5453
// Trailing slash is used to allow the path to be treated as a directory
55-
const workspaceRequire = nodeModule.createRequire(this.context.root + '/');
54+
const workspaceRequire = nodeModule.createRequire(root + '/');
5655

5756
const cliPackage: PartialPackageInfo = localRequire('./package.json');
5857
let workspacePackage: PartialPackageInfo | undefined;
@@ -119,7 +118,7 @@ export class VersionCommandModule extends CommandModule implements CommandModule
119118
`
120119
Angular CLI: ${ngCliVersion}
121120
Node: ${process.versions.node}${unsupportedNodeVersion ? ' (Unsupported)' : ''}
122-
Package Manager: ${this.getPackageManagerVersion()}
121+
Package Manager: ${packageManager.name} ${packageManager.version ?? '<error>'}
123122
OS: ${process.platform} ${process.arch}
124123
125124
Angular: ${angularCoreVersion}
@@ -186,24 +185,4 @@ export class VersionCommandModule extends CommandModule implements CommandModule
186185

187186
return '<error>';
188187
}
189-
190-
private getPackageManagerVersion(): string {
191-
try {
192-
const manager = this.context.packageManager;
193-
const version = execSync(`${manager} --version`, {
194-
encoding: 'utf8',
195-
stdio: ['ignore', 'pipe', 'ignore'],
196-
env: {
197-
...process.env,
198-
// NPM updater notifier will prevents the child process from closing until it timeout after 3 minutes.
199-
NO_UPDATE_NOTIFIER: '1',
200-
NPM_CONFIG_UPDATE_NOTIFIER: 'false',
201-
},
202-
}).trim();
203-
204-
return `${manager} ${version}`;
205-
} catch {
206-
return '<error>';
207-
}
208-
}
209188
}

Diff for: packages/angular/cli/src/utilities/config.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -150,22 +150,20 @@ export class AngularWorkspace {
150150
}
151151
}
152152

153-
const cachedWorkspaces = new Map<string, AngularWorkspace | null>();
154-
153+
const cachedWorkspaces = new Map<string, AngularWorkspace | undefined>();
155154
export async function getWorkspace(
156155
level: 'local' | 'global' = 'local',
157-
): Promise<AngularWorkspace | null> {
158-
const cached = cachedWorkspaces.get(level);
159-
if (cached !== undefined) {
160-
return cached;
156+
): Promise<AngularWorkspace | undefined> {
157+
if (cachedWorkspaces.has(level)) {
158+
return cachedWorkspaces.get(level);
161159
}
162160

163161
const configPath = level === 'local' ? projectFilePath() : globalFilePath();
164162

165163
if (!configPath) {
166-
cachedWorkspaces.set(level, null);
164+
cachedWorkspaces.set(level, undefined);
167165

168-
return null;
166+
return undefined;
169167
}
170168

171169
try {

0 commit comments

Comments
 (0)