diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel index 84d6152df57a..20b1b6529b60 100644 --- a/packages/angular/cli/BUILD.bazel +++ b/packages/angular/cli/BUILD.bazel @@ -79,6 +79,7 @@ ts_library( "@npm//@types/uuid", "@npm//ansi-colors", "@npm//jsonc-parser", + "@npm//ora", ], ) diff --git a/packages/angular/cli/commands/add-impl.ts b/packages/angular/cli/commands/add-impl.ts index 5e1da5855055..79d0c6155cc5 100644 --- a/packages/angular/cli/commands/add-impl.ts +++ b/packages/angular/cli/commands/add-impl.ts @@ -22,6 +22,7 @@ import { fetchPackageManifest, fetchPackageMetadata, } from '../utilities/package-metadata'; +import { Spinner } from '../utilities/spinner'; import { Schema as AddCommandSchema } from './add'; const npa = require('npm-package-arg'); @@ -79,12 +80,18 @@ export class AddCommand extends SchematicCommand { } } + const spinner = new Spinner(); + + spinner.start('Determining package manager...'); const packageManager = await getPackageManager(this.context.root); const usingYarn = packageManager === PackageManager.Yarn; + spinner.info(`Using package manager: ${colors.grey(packageManager)}`); if (packageIdentifier.type === 'tag' && !packageIdentifier.rawSpec) { // only package name provided; search for viable version // plus special cases for packages that did not have peer deps setup + spinner.start('Searching for compatible package version...'); + let packageMetadata; try { packageMetadata = await fetchPackageMetadata(packageIdentifier.name, this.logger, { @@ -93,7 +100,7 @@ export class AddCommand extends SchematicCommand { verbose: options.verbose, }); } catch (e) { - this.logger.error('Unable to fetch package metadata: ' + e.message); + spinner.fail('Unable to load package information from registry: ' + e.message); return 1; } @@ -111,7 +118,10 @@ export class AddCommand extends SchematicCommand { ) { packageIdentifier = npa.resolve('@angular/pwa', '0.12'); } + } else { + packageIdentifier = npa.resolve(latestManifest.name, latestManifest.version); } + spinner.succeed(`Found compatible package version: ${colors.grey(packageIdentifier)}.`); } else if (!latestManifest || (await this.hasMismatchedPeer(latestManifest))) { // 'latest' is invalid so search for most recent matching package const versionManifests = Object.values(packageMetadata.versions).filter( @@ -129,10 +139,14 @@ export class AddCommand extends SchematicCommand { } if (!newIdentifier) { - this.logger.warn("Unable to find compatible package. Using 'latest'."); + spinner.warn("Unable to find compatible package. Using 'latest'."); } else { packageIdentifier = newIdentifier; + spinner.succeed(`Found compatible package version: ${colors.grey(packageIdentifier)}.`); } + } else { + packageIdentifier = npa.resolve(latestManifest.name, latestManifest.version); + spinner.succeed(`Found compatible package version: ${colors.grey(packageIdentifier)}.`); } } @@ -140,6 +154,7 @@ export class AddCommand extends SchematicCommand { let savePackage: NgAddSaveDepedency | undefined; try { + spinner.start('Loading package information from registry...'); const manifest = await fetchPackageManifest(packageIdentifier, this.logger, { registry: options.registry, verbose: options.verbose, @@ -150,41 +165,51 @@ export class AddCommand extends SchematicCommand { collectionName = manifest.name; if (await this.hasMismatchedPeer(manifest)) { - this.logger.warn( + spinner.warn( 'Package has unmet peer dependencies. Adding the package may not succeed.', ); + } else { + spinner.succeed(`Package information loaded.`); } } catch (e) { - this.logger.error('Unable to fetch package manifest: ' + e.message); + spinner.fail(`Unable to fetch package information for '${packageIdentifier}': ${e.message}`); return 1; } - if (savePackage === false) { - // Temporary packages are located in a different directory - // Hence we need to resolve them using the temp path - const tempPath = installTempPackage( - packageIdentifier.raw, - this.logger, - packageManager, - options.registry ? [`--registry="${options.registry}"`] : undefined, - ); - const resolvedCollectionPath = require.resolve( - join(collectionName, 'package.json'), - { - paths: [tempPath], - }, - ); + try { + spinner.start('Installing package...'); + if (savePackage === false) { + // Temporary packages are located in a different directory + // Hence we need to resolve them using the temp path + const tempPath = installTempPackage( + packageIdentifier.raw, + undefined, + packageManager, + options.registry ? [`--registry="${options.registry}"`] : undefined, + ); + const resolvedCollectionPath = require.resolve( + join(collectionName, 'package.json'), + { + paths: [tempPath], + }, + ); - collectionName = dirname(resolvedCollectionPath); - } else { - installPackage( - packageIdentifier.raw, - this.logger, - packageManager, - savePackage, - options.registry ? [`--registry="${options.registry}"`] : undefined, - ); + collectionName = dirname(resolvedCollectionPath); + } else { + installPackage( + packageIdentifier.raw, + undefined, + packageManager, + savePackage, + options.registry ? [`--registry="${options.registry}"`] : undefined, + ); + } + spinner.succeed('Package successfully installed.'); + } catch (error) { + spinner.fail(`Package installation failed: ${error.message}`); + + return 1; } return this.executeSchematic(collectionName, options['--']); diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 1a3ceabe0ccd..6bbc9fec3507 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -39,6 +39,7 @@ "npm-package-arg": "8.1.0", "npm-pick-manifest": "6.1.0", "open": "7.3.1", + "ora": "5.3.0", "pacote": "11.2.3", "resolve": "1.19.0", "rimraf": "3.0.2", diff --git a/packages/angular/cli/utilities/install-package.ts b/packages/angular/cli/utilities/install-package.ts index a4862a3966b3..047a682cd9f7 100644 --- a/packages/angular/cli/utilities/install-package.ts +++ b/packages/angular/cli/utilities/install-package.ts @@ -26,7 +26,7 @@ interface PackageManagerOptions { export function installPackage( packageName: string, - logger: logging.Logger, + logger: logging.Logger | undefined, packageManager: PackageManager = PackageManager.Npm, save: Exclude = true, extraArgs: string[] = [], @@ -40,7 +40,7 @@ export function installPackage( packageManagerArgs.silent, ]; - logger.info(colors.green(`Installing packages for tooling via ${packageManager}.`)); + logger?.info(colors.green(`Installing packages for tooling via ${packageManager}.`)); if (save === 'devDependencies') { installArgs.push(packageManagerArgs.saveDev); @@ -61,12 +61,12 @@ export function installPackage( throw new Error(errorMessage + `Package install failed${errorMessage ? ', see above' : ''}.`); } - logger.info(colors.green(`Installed packages for tooling via ${packageManager}.`)); + logger?.info(colors.green(`Installed packages for tooling via ${packageManager}.`)); } export function installTempPackage( packageName: string, - logger: logging.Logger, + logger: logging.Logger | undefined, packageManager: PackageManager = PackageManager.Npm, extraArgs?: string[], ): string { diff --git a/packages/angular/cli/utilities/spinner.ts b/packages/angular/cli/utilities/spinner.ts new file mode 100644 index 000000000000..f7b8d8550662 --- /dev/null +++ b/packages/angular/cli/utilities/spinner.ts @@ -0,0 +1,59 @@ +/** + * @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 + */ + +import * as ora from 'ora'; +import { colors } from './color'; + +export class Spinner { + private readonly spinner: ora.Ora; + + /** When false, only fail messages will be displayed. */ + enabled = true; + + constructor(text?: string) { + this.spinner = ora({ + text, + // The below 2 options are needed because otherwise CTRL+C will be delayed + // when the underlying process is sync. + hideCursor: false, + discardStdin: false, + }); + } + + set text(text: string) { + this.spinner.text = text; + } + + succeed(text?: string): void { + if (this.enabled) { + this.spinner.succeed(text); + } + } + + info(text?: string): void { + this.spinner.info(text); + } + + fail(text?: string): void { + this.spinner.fail(text && colors.redBright(text)); + } + + warn(text?: string): void { + this.spinner.fail(text && colors.yellowBright(text)); + } + + stop(): void { + this.spinner.stop(); + } + + start(text?: string): void { + if (this.enabled) { + this.spinner.start(text); + } + } +}