Skip to content

Commit ecd9fb5

Browse files
clydinfilipesilva
authored andcommitted
feat(@angular/cli): provide more detailed error for not found builder
When a builder-based command is executed (build, serve, test, etc.) and the builder's node package cannot be found a more user-friendly error message is now displayed. In addition, when the builder's node package cannot be found, a check is performed to determine if the node packages for the workspace may have not been installed. Previously, a potentially long stacktrace was shown which did not provide much information regarding how to correct the issue. Closes: #10536
1 parent 1e81b8d commit ecd9fb5

File tree

2 files changed

+107
-3
lines changed

2 files changed

+107
-3
lines changed

packages/angular/cli/models/architect-command.ts

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
import { Architect, Target } from '@angular-devkit/architect';
1010
import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node';
1111
import { json, schema, tags } from '@angular-devkit/core';
12+
import { existsSync } from 'fs';
13+
import * as path from 'path';
1214
import { parseJsonSchemaToOptions } from '../utilities/json-schema';
15+
import { getPackageManager } from '../utilities/package-manager';
1316
import { isPackageNameSafeForAnalytics } from './analytics';
1417
import { BaseCommandOptions, Command } from './command';
1518
import { Arguments, Option } from './interface';
@@ -115,7 +118,19 @@ export abstract class ArchitectCommand<
115118
builderNames.add(builderName);
116119
}
117120

118-
const builderDesc = await this._architectHost.resolveBuilder(builderName);
121+
let builderDesc;
122+
try {
123+
builderDesc = await this._architectHost.resolveBuilder(builderName);
124+
} catch (e) {
125+
if (e.code === 'MODULE_NOT_FOUND') {
126+
await this.warnOnMissingNodeModules(this.workspace.basePath);
127+
this.logger.fatal(`Could not find the '${builderName}' builder's node package.`);
128+
129+
return 1;
130+
}
131+
throw e;
132+
}
133+
119134
const optionDefs = await parseJsonSchemaToOptions(
120135
this._registry,
121136
builderDesc.optionSchema as json.JsonObject,
@@ -193,7 +208,19 @@ export abstract class ArchitectCommand<
193208
project: projectName || (targetProjectNames.length > 0 ? targetProjectNames[0] : ''),
194209
target: this.target,
195210
});
196-
const builderDesc = await this._architectHost.resolveBuilder(builderConf);
211+
212+
let builderDesc;
213+
try {
214+
builderDesc = await this._architectHost.resolveBuilder(builderConf);
215+
} catch (e) {
216+
if (e.code === 'MODULE_NOT_FOUND') {
217+
await this.warnOnMissingNodeModules(this.workspace.basePath);
218+
this.logger.fatal(`Could not find the '${builderConf}' builder's node package.`);
219+
220+
return 1;
221+
}
222+
throw e;
223+
}
197224

198225
this.description.options.push(
199226
...(await parseJsonSchemaToOptions(
@@ -210,6 +237,38 @@ export abstract class ArchitectCommand<
210237
}
211238
}
212239

240+
private async warnOnMissingNodeModules(basePath: string): Promise<void> {
241+
// Check for a `node_modules` directory (npm, yarn non-PnP, etc.)
242+
if (existsSync(path.resolve(basePath, 'node_modules'))) {
243+
return;
244+
}
245+
246+
// Check for yarn PnP files
247+
if (
248+
existsSync(path.resolve(basePath, '.pnp.js')) ||
249+
existsSync(path.resolve(basePath, '.pnp.cjs')) ||
250+
existsSync(path.resolve(basePath, '.pnp.mjs'))
251+
) {
252+
return;
253+
}
254+
255+
const packageManager = await getPackageManager(basePath);
256+
let installSuggestion = 'Try installing with ';
257+
switch (packageManager) {
258+
case 'npm':
259+
installSuggestion += `'npm install'`;
260+
break;
261+
case 'yarn':
262+
installSuggestion += `'yarn'`;
263+
break;
264+
default:
265+
installSuggestion += `the project's package manager`;
266+
break;
267+
}
268+
269+
this.logger.warn(`Node packages may not be installed. ${installSuggestion}.`);
270+
}
271+
213272
async run(options: ArchitectCommandOptions & Arguments) {
214273
return await this.runArchitectTarget(options);
215274
}
@@ -219,7 +278,19 @@ export abstract class ArchitectCommand<
219278
// overrides separately (getting the configuration builds the whole project, including
220279
// overrides).
221280
const builderConf = await this._architectHost.getBuilderNameForTarget(target);
222-
const builderDesc = await this._architectHost.resolveBuilder(builderConf);
281+
let builderDesc;
282+
try {
283+
builderDesc = await this._architectHost.resolveBuilder(builderConf);
284+
} catch (e) {
285+
if (e.code === 'MODULE_NOT_FOUND') {
286+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
287+
await this.warnOnMissingNodeModules(this.workspace!.basePath);
288+
this.logger.fatal(`Could not find the '${builderConf}' builder's node package.`);
289+
290+
return 1;
291+
}
292+
throw e;
293+
}
223294
const targetOptionArray = await parseJsonSchemaToOptions(
224295
this._registry,
225296
builderDesc.optionSchema as json.JsonObject,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { moveFile } from '../../utils/fs';
2+
import { installPackage, uninstallPackage } from '../../utils/packages';
3+
import { execAndWaitForOutputToMatch, ng } from '../../utils/process';
4+
import { expectToFail } from '../../utils/utils';
5+
6+
export default async function () {
7+
try {
8+
await uninstallPackage('@angular-devkit/build-angular');
9+
10+
await expectToFail(() => ng('build'));
11+
await execAndWaitForOutputToMatch(
12+
'ng',
13+
['build'],
14+
/Could not find the '@angular-devkit\/build-angular:browser' builder's node package\./,
15+
);
16+
await expectToFail(() =>
17+
execAndWaitForOutputToMatch('ng', ['build'], /Node packages may not be installed\./),
18+
);
19+
20+
await moveFile('node_modules', 'temp_node_modules');
21+
22+
await expectToFail(() => ng('build'));
23+
await execAndWaitForOutputToMatch(
24+
'ng',
25+
['build'],
26+
/Could not find the '@angular-devkit\/build-angular:browser' builder's node package\./,
27+
);
28+
await execAndWaitForOutputToMatch('ng', ['build'], /Node packages may not be installed\./);
29+
} finally {
30+
await moveFile('temp_node_modules', 'node_modules');
31+
await installPackage('@angular-devkit/build-angular');
32+
}
33+
}

0 commit comments

Comments
 (0)