Skip to content

Commit f722aff

Browse files
committed
fix(@angular/cli): gracefully handle package manager errors in command handler
Catch `PackageManagerError` in the global `CommandModule` handler and gracefully log the installation failure message along with the process output (stdout/stderr) using `logger.fatal`. This ensures that any package manager error thrown during the execution of a command is reported cleanly to the terminal and sets the exit code to 1.
1 parent 75c1dce commit f722aff

2 files changed

Lines changed: 38 additions & 49 deletions

File tree

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Parser as yargsParser } from 'yargs/helpers';
1414
import { getAnalyticsUserId } from '../analytics/analytics';
1515
import { AnalyticsCollector } from '../analytics/analytics-collector';
1616
import { EventCustomDimension, EventCustomMetric } from '../analytics/analytics-parameters';
17+
import { PackageManagerError } from '../package-managers';
1718
import { considerSettingUpAutocompletion } from '../utilities/completion';
1819
import { AngularWorkspace } from '../utilities/config';
1920
import { memoize } from '../utilities/memoize';
@@ -95,6 +96,7 @@ export abstract class CommandModule<T extends {} = {}> implements CommandModuleI
9596

9697
async handler(args: ArgumentsCamelCase<T> & OtherOptions): Promise<void> {
9798
const { _, $0, ...options } = args;
99+
const { logger } = this.context;
98100

99101
// Camelize options as yargs will return the object in kebab-case when camel casing is disabled.
100102
const camelCasedOptions: Record<string, unknown> = {};
@@ -103,10 +105,7 @@ export abstract class CommandModule<T extends {} = {}> implements CommandModuleI
103105
}
104106

105107
// Set up autocompletion if appropriate.
106-
const autocompletionExitCode = await considerSettingUpAutocompletion(
107-
this.commandName,
108-
this.context.logger,
109-
);
108+
const autocompletionExitCode = await considerSettingUpAutocompletion(this.commandName, logger);
110109
if (autocompletionExitCode !== undefined) {
111110
process.exitCode = autocompletionExitCode;
112111

@@ -127,7 +126,13 @@ export abstract class CommandModule<T extends {} = {}> implements CommandModuleI
127126
exitCode = await this.run(camelCasedOptions as Options<T> & OtherOptions);
128127
} catch (e) {
129128
if (e instanceof schema.SchemaValidationException) {
130-
this.context.logger.fatal(`Error: ${e.message}`);
129+
logger.fatal(`Error: ${e.message}`);
130+
exitCode = 1;
131+
} else if (e instanceof PackageManagerError) {
132+
const output = e.stderr || e.stdout;
133+
logger.fatal(
134+
`Error: Package installation failed: ${e.message}${output ? `\nOutput: ${output}` : ''}`,
135+
);
131136
exitCode = 1;
132137
} else {
133138
throw e;

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

Lines changed: 28 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,7 @@ import {
2525
SchematicsCommandArgs,
2626
SchematicsCommandModule,
2727
} from '../../command-builder/schematics-command-module';
28-
import {
29-
NgAddSaveDependency,
30-
PackageManagerError,
31-
PackageManifest,
32-
PackageMetadata,
33-
} from '../../package-managers';
28+
import { NgAddSaveDependency, PackageManifest, PackageMetadata } from '../../package-managers';
3429
import { assertIsError } from '../../utilities/error';
3530
import { isTTY } from '../../utilities/tty';
3631
import { VERSION } from '../../utilities/version';
@@ -639,47 +634,36 @@ export default class AddCommandModule
639634
// Only show if installation will actually occur
640635
task.title = 'Installing package';
641636

642-
try {
643-
if (context.savePackage === false) {
644-
task.title += ' in temporary location';
645-
646-
// Temporary packages are located in a different directory
647-
// Hence we need to resolve them using the temp path
648-
const { workingDirectory } = await packageManager.acquireTempPackage(
649-
packageIdentifier.toString(),
650-
{
651-
registry,
652-
},
653-
);
637+
if (context.savePackage === false) {
638+
task.title += ' in temporary location';
654639

655-
const tempRequire = createRequire(workingDirectory + '/');
656-
assert(context.collectionName, 'Collection name should always be available');
657-
const resolvedCollectionPath = tempRequire.resolve(
658-
join(context.collectionName, 'package.json'),
659-
);
640+
// Temporary packages are located in a different directory
641+
// Hence we need to resolve them using the temp path
642+
const { workingDirectory } = await packageManager.acquireTempPackage(
643+
packageIdentifier.toString(),
644+
{
645+
registry,
646+
},
647+
);
660648

661-
context.collectionName = dirname(resolvedCollectionPath);
662-
} else {
663-
await packageManager.add(
664-
packageIdentifier.toString(),
665-
'none',
666-
savePackage === 'devDependencies',
667-
false,
668-
true,
669-
{
670-
registry,
671-
},
672-
);
673-
}
674-
} catch (e) {
675-
if (e instanceof PackageManagerError) {
676-
const output = e.stderr || e.stdout;
677-
if (output) {
678-
throw new CommandError(`Package installation failed: ${e.message}\nOutput: ${output}`);
679-
}
680-
}
649+
const tempRequire = createRequire(workingDirectory + '/');
650+
assert(context.collectionName, 'Collection name should always be available');
651+
const resolvedCollectionPath = tempRequire.resolve(
652+
join(context.collectionName, 'package.json'),
653+
);
681654

682-
throw e;
655+
context.collectionName = dirname(resolvedCollectionPath);
656+
} else {
657+
await packageManager.add(
658+
packageIdentifier.toString(),
659+
'none',
660+
savePackage === 'devDependencies',
661+
false,
662+
true,
663+
{
664+
registry,
665+
},
666+
);
683667
}
684668
}
685669

0 commit comments

Comments
 (0)