Skip to content

Commit 0d53e82

Browse files
committed
feat(@angular/cli): provide detailed peer dependency conflict errors in ng add
This commit enhances the `ng add` command's version resolution to provide more informative feedback to the user, especially when peer dependency conflicts occur. Key improvements include: - When a compatible version cannot be found, the command now lists the specific peer dependency conflicts that caused recent versions to be rejected. By default, up to 5 conflicts are shown. - The peer dependency check now collects and reports all conflicts for a given package version, not just the first one it finds. - In verbose mode (`--verbose`), all detected peer dependency conflicts will be displayed.
1 parent a8b049a commit 0d53e82

File tree

1 file changed

+77
-57
lines changed
  • packages/angular/cli/src/commands/add

1 file changed

+77
-57
lines changed

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

Lines changed: 77 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ interface AddCommandTaskContext {
4949
savePackage?: NgAddSaveDependency;
5050
collectionName?: string;
5151
executeSchematic: AddCommandModule['executeSchematic'];
52-
hasMismatchedPeer: AddCommandModule['hasMismatchedPeer'];
52+
getPeerDependencyConflicts: AddCommandModule['getPeerDependencyConflicts'];
5353
}
5454

5555
type AddCommandTaskWrapper = ListrTaskWrapper<
@@ -70,6 +70,8 @@ const packageVersionExclusions: Record<string, string | Range> = {
7070
'@angular/material': '7.x',
7171
};
7272

73+
const DEFAULT_CONFLICT_DISPLAY_LIMIT = 5;
74+
7375
export default class AddCommandModule
7476
extends SchematicsCommandModule
7577
implements CommandModuleImplementation<AddCommandArgs>
@@ -158,7 +160,7 @@ export default class AddCommandModule
158160
const taskContext: AddCommandTaskContext = {
159161
packageIdentifier,
160162
executeSchematic: this.executeSchematic.bind(this),
161-
hasMismatchedPeer: this.hasMismatchedPeer.bind(this),
163+
getPeerDependencyConflicts: this.getPeerDependencyConflicts.bind(this),
162164
};
163165

164166
const tasks = new Listr<AddCommandTaskContext>(
@@ -248,69 +250,83 @@ export default class AddCommandModule
248250
throw new CommandError(`Unable to load package information from registry: ${e.message}`);
249251
}
250252

253+
const rejectionReasons: string[] = [];
254+
251255
// Start with the version tagged as `latest` if it exists
252256
const latestManifest = packageMetadata.tags['latest'];
253257
if (latestManifest) {
254-
context.packageIdentifier = npa.resolve(latestManifest.name, latestManifest.version);
258+
const latestConflicts = await this.getPeerDependencyConflicts(latestManifest);
259+
if (latestConflicts) {
260+
// 'latest' is invalid so search for most recent matching package
261+
rejectionReasons.push(...latestConflicts);
262+
} else {
263+
context.packageIdentifier = npa.resolve(latestManifest.name, latestManifest.version);
264+
task.output = `Found compatible package version: ${color.blue(latestManifest.version)}.`;
265+
266+
return;
267+
}
255268
}
256269

257-
// Adjust the version based on name and peer dependencies
258-
if (
259-
latestManifest?.peerDependencies &&
260-
Object.keys(latestManifest.peerDependencies).length === 0
261-
) {
262-
task.output = `Found compatible package version: ${color.blue(latestManifest.version)}.`;
263-
} else if (!latestManifest || (await context.hasMismatchedPeer(latestManifest))) {
264-
// 'latest' is invalid so search for most recent matching package
265-
266-
// Allow prelease versions if the CLI itself is a prerelease
267-
const allowPrereleases = prerelease(VERSION.full);
268-
269-
const versionExclusions = packageVersionExclusions[packageMetadata.name];
270-
const versionManifests = Object.values(packageMetadata.versions).filter(
271-
(value: PackageManifest) => {
272-
// Prerelease versions are not stable and should not be considered by default
273-
if (!allowPrereleases && prerelease(value.version)) {
274-
return false;
275-
}
276-
// Deprecated versions should not be used or considered
277-
if (value.deprecated) {
278-
return false;
279-
}
280-
// Excluded package versions should not be considered
281-
if (
282-
versionExclusions &&
283-
satisfies(value.version, versionExclusions, { includePrerelease: true })
284-
) {
285-
return false;
286-
}
270+
// Allow prelease versions if the CLI itself is a prerelease
271+
const allowPrereleases = prerelease(VERSION.full);
287272

288-
return true;
289-
},
290-
);
273+
const versionExclusions = packageVersionExclusions[packageMetadata.name];
274+
const versionManifests = Object.values(packageMetadata.versions).filter(
275+
(value: PackageManifest) => {
276+
// Already checked the 'latest' version
277+
if (latestManifest.version === value.version) {
278+
return false;
279+
}
280+
// Prerelease versions are not stable and should not be considered by default
281+
if (!allowPrereleases && prerelease(value.version)) {
282+
return false;
283+
}
284+
// Deprecated versions should not be used or considered
285+
if (value.deprecated) {
286+
return false;
287+
}
288+
// Excluded package versions should not be considered
289+
if (
290+
versionExclusions &&
291+
satisfies(value.version, versionExclusions, { includePrerelease: true })
292+
) {
293+
return false;
294+
}
291295

292-
// Sort in reverse SemVer order so that the newest compatible version is chosen
293-
versionManifests.sort((a, b) => compare(b.version, a.version, true));
296+
return true;
297+
},
298+
);
294299

295-
let found = false;
296-
for (const versionManifest of versionManifests) {
297-
const mismatch = await context.hasMismatchedPeer(versionManifest);
298-
if (mismatch) {
299-
continue;
300-
}
300+
// Sort in reverse SemVer order so that the newest compatible version is chosen
301+
versionManifests.sort((a, b) => compare(b.version, a.version, true));
301302

302-
context.packageIdentifier = npa.resolve(versionManifest.name, versionManifest.version);
303-
found = true;
304-
break;
303+
let found = false;
304+
for (const versionManifest of versionManifests) {
305+
const conflicts = await this.getPeerDependencyConflicts(versionManifest);
306+
if (conflicts) {
307+
if (options.verbose || rejectionReasons.length < DEFAULT_CONFLICT_DISPLAY_LIMIT) {
308+
rejectionReasons.push(...conflicts);
309+
}
310+
continue;
305311
}
306312

307-
if (!found) {
308-
task.output = "Unable to find compatible package. Using 'latest' tag.";
309-
} else {
310-
task.output = `Found compatible package version: ${color.blue(
311-
context.packageIdentifier.toString(),
312-
)}.`;
313+
context.packageIdentifier = npa.resolve(versionManifest.name, versionManifest.version);
314+
found = true;
315+
break;
316+
}
317+
318+
if (!found) {
319+
let message = `Unable to find compatible package. Using 'latest' tag.`;
320+
if (rejectionReasons.length > 0) {
321+
message +=
322+
'\nThis is often because of incompatible peer dependencies.\n' +
323+
'These versions were rejected due to the following conflicts:\n' +
324+
rejectionReasons
325+
.slice(0, options.verbose ? undefined : DEFAULT_CONFLICT_DISPLAY_LIMIT)
326+
.map((r) => ` - ${r}`)
327+
.join('\n');
313328
}
329+
task.output = message;
314330
} else {
315331
task.output = `Found compatible package version: ${color.blue(
316332
context.packageIdentifier.toString(),
@@ -343,7 +359,7 @@ export default class AddCommandModule
343359
context.savePackage = manifest['ng-add']?.save;
344360
context.collectionName = manifest.name;
345361

346-
if (await context.hasMismatchedPeer(manifest)) {
362+
if (await this.getPeerDependencyConflicts(manifest)) {
347363
task.output = color.yellow(
348364
figures.warning +
349365
' Package has unmet peer dependencies. Adding the package may not succeed.',
@@ -563,7 +579,8 @@ export default class AddCommandModule
563579
return null;
564580
}
565581

566-
private async hasMismatchedPeer(manifest: PackageManifest): Promise<boolean> {
582+
private async getPeerDependencyConflicts(manifest: PackageManifest): Promise<string[] | false> {
583+
const conflicts: string[] = [];
567584
for (const peer in manifest.peerDependencies) {
568585
let peerIdentifier;
569586
try {
@@ -586,7 +603,10 @@ export default class AddCommandModule
586603
!intersects(version, peerIdentifier.rawSpec, options) &&
587604
!satisfies(version, peerIdentifier.rawSpec, options)
588605
) {
589-
return true;
606+
conflicts.push(
607+
`Package "${manifest.name}@${manifest.version}" has an incompatible peer dependency to "` +
608+
`${peer}@${peerIdentifier.rawSpec}" (requires "${version}" in project).`,
609+
);
590610
}
591611
} catch {
592612
// Not found or invalid so ignore
@@ -598,6 +618,6 @@ export default class AddCommandModule
598618
}
599619
}
600620

601-
return false;
621+
return conflicts.length > 0 && conflicts;
602622
}
603623
}

0 commit comments

Comments
 (0)