Skip to content

Commit

Permalink
feat(@angular/cli): provide additional status messaging for ng add
Browse files Browse the repository at this point in the history
This adds a spinner as well as shows more information regarding what package version was selected to be installed.

Closes angular#17983
  • Loading branch information
clydin committed Jan 26, 2021
1 parent e7e01d2 commit 941ba8f
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 32 deletions.
81 changes: 53 additions & 28 deletions packages/angular/cli/commands/add-impl.ts
Expand Up @@ -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');
Expand Down Expand Up @@ -79,12 +80,18 @@ export class AddCommand extends SchematicCommand<AddCommandSchema> {
}
}

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, {
Expand All @@ -93,7 +100,7 @@ export class AddCommand extends SchematicCommand<AddCommandSchema> {
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;
}
Expand All @@ -111,7 +118,10 @@ export class AddCommand extends SchematicCommand<AddCommandSchema> {
) {
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(
Expand All @@ -129,17 +139,22 @@ export class AddCommand extends SchematicCommand<AddCommandSchema> {
}

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)}`);
}
}

let collectionName = packageIdentifier.name;
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,
Expand All @@ -150,41 +165,51 @@ export class AddCommand extends SchematicCommand<AddCommandSchema> {
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['--']);
Expand Down
1 change: 1 addition & 0 deletions packages/angular/cli/package.json
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions packages/angular/cli/utilities/install-package.ts
Expand Up @@ -26,7 +26,7 @@ interface PackageManagerOptions {

export function installPackage(
packageName: string,
logger: logging.Logger,
logger: logging.Logger | undefined,
packageManager: PackageManager = PackageManager.Npm,
save: Exclude<NgAddSaveDepedency, false> = true,
extraArgs: string[] = [],
Expand All @@ -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);
Expand All @@ -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 {
Expand Down
59 changes: 59 additions & 0 deletions 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);
}
}
}

0 comments on commit 941ba8f

Please sign in to comment.