Skip to content

Commit 0e932cf

Browse files
author
Samir Boulema
committed
feat: Show number of updates, deprecations and vulnerabilities
1 parent c5f3e61 commit 0e932cf

9 files changed

+119
-61
lines changed

Diff for: .github/workflows/workflow.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
- 'feature/**'
88

99
env:
10-
version: '1.0.${{ github.run_number }}'
10+
version: '1.1.${{ github.run_number }}'
1111
repoUrl: ${{ github.server_url }}/${{ github.repository }}
1212

1313
jobs:

Diff for: README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ A big thanks goes to [AnushaG2201](https://github.com/AnushaG2201)!
2525

2626
I was playing with the idea for this extension for quite a while but never figured out how I would create this extension.
2727

28-
That is until I saw the [Nuget-updates-notifier](https://marketplace.visualstudio.com/items?itemName=Anusha.NugetPackageUpdateNotifier) ([Github](https://github.com/AnushaG2201/Nuget-updates-notifier)) which gave mt the remaining puzzle pieces, so that I could create my own version.
28+
That is until I saw the [Nuget-updates-notifier](https://marketplace.visualstudio.com/items?itemName=Anusha.NugetPackageUpdateNotifier) ([Github](https://github.com/AnushaG2201/Nuget-updates-notifier)) which gave me the remaining puzzle pieces, so that I could create my own version.
2929

3030
## Links
3131
[NuGet Client SDK / NuGet.Protocol](https://learn.microsoft.com/en-us/nuget/reference/nuget-client-sdk)
3232

3333
[Visual Studio Extensibility Cookbook - Notifications](https://www.vsixcookbook.com/recipes/notifications.html)
3434

35-
[Invoke the Manage NuGet Packages dialog programmatically](https://devblogs.microsoft.com/nuget/invoke-manage-nuget-packages-dialog-programmatically/)
35+
[Invoke the Manage NuGet Packages dialog programmatically](https://devblogs.microsoft.com/nuget/invoke-manage-nuget-packages-dialog-programmatically/)
36+
37+
[UpdatR packages](https://github.com/OskarKlintrot/UpdatR)

Diff for: src/Models/PackageReference.cs

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
1-
using System;
1+
using NuGet.Packaging.Core;
2+
using NuGet.Versioning;
3+
using System.Xml.Linq;
24

35
namespace NuGetMonitor.Models
46
{
57
public class PackageReference
68
{
7-
public string Include { get; set; } = string.Empty;
9+
public PackageReference(XElement packageReference)
10+
{
11+
PackageIdentity = new PackageIdentity(
12+
packageReference.Attribute("Include").Value,
13+
new NuGetVersion(packageReference.Attribute("Version").Value));
14+
}
815

9-
public Version Version { get; set; }
16+
public PackageIdentity PackageIdentity { get; set; }
17+
18+
public bool IsVulnerable { get; set; }
19+
20+
public bool IsDeprecated { get; set; }
21+
22+
public bool IsOutdated { get; set; }
1023
}
1124
}

Diff for: src/NuGetMonitorPackage.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace NuGetMonitor
1111
{
1212
[Guid(PackageGuidString)]
1313
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
14-
[ProvideAutoLoad(VSConstants.UICONTEXT.NoSolution_string, PackageAutoLoadFlags.BackgroundLoad)]
14+
[ProvideAutoLoad(VSConstants.UICONTEXT.SolutionExistsAndFullyLoaded_string, PackageAutoLoadFlags.BackgroundLoad)]
1515
public sealed class NuGetMonitorPackage : ToolkitPackage
1616
{
1717
public const string PackageGuidString = "38279e01-6b27-4a29-9221-c4ea8748f16e";
@@ -21,6 +21,8 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke
2121
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
2222

2323
MonitorService.RegisterEventHandler();
24+
25+
MonitorService.CheckForUpdates().FireAndForget();
2426
}
2527
}
2628
}

Diff for: src/Services/InfoBarService.cs

+49-8
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,31 @@
33
using Microsoft.VisualStudio.Shell.Interop;
44
using Microsoft.VisualStudio.Shell;
55
using System.Threading.Tasks;
6+
using NuGetMonitor.Models;
7+
using System.Collections.Generic;
8+
using System.Linq;
69

710
namespace NuGetMonitor.Services
811
{
912
public class InfoBarService
1013
{
11-
public static async Task ShowInfoBar()
14+
public static async Task ShowInfoBar(IEnumerable<PackageReference> packageReferences)
1215
{
16+
var outdatedCount = packageReferences.Count(packageRefence => packageRefence.IsOutdated);
17+
var deprecatedCount = packageReferences.Count(packageRefence => packageRefence.IsDeprecated);
18+
var vulnerableCount = packageReferences.Count(packageRefence => packageRefence.IsVulnerable);
19+
20+
if (outdatedCount == 0 &&
21+
deprecatedCount == 0 &&
22+
vulnerableCount == 0)
23+
{
24+
return;
25+
}
26+
1327
var model = new InfoBarModel(
14-
new[] {
15-
new InfoBarTextSpan("NuGet updates available. "),
16-
new InfoBarHyperlink("Manage NuGet"),
17-
new InfoBarTextSpan(".")
18-
},
28+
GetTextSpans(outdatedCount, deprecatedCount, vulnerableCount),
1929
KnownMonikers.NuGet,
20-
true);
30+
isCloseButtonVisible: true);
2131

2232
var infoBar = await VS.InfoBar.CreateAsync(ToolWindowGuids80.SolutionExplorer, model);
2333
infoBar.ActionItemClicked += InfoBar_ActionItemClicked;
@@ -29,10 +39,41 @@ private static void InfoBar_ActionItemClicked(object sender, InfoBarActionItemEv
2939
{
3040
ThreadHelper.ThrowIfNotOnUIThread();
3141

32-
if (e.ActionItem.Text == "Manage NuGet")
42+
if (e.ActionItem.Text == "Manage")
3343
{
3444
VS.Commands.ExecuteAsync("Tools.ManageNuGetPackagesForSolution").FireAndForget();
3545
}
46+
47+
(sender as InfoBar).Close();
48+
}
49+
50+
private static List<IVsInfoBarTextSpan> GetTextSpans(int outdatedCount, int deprecatedCount, int vulnerableCount)
51+
{
52+
// Idea for showing counts, not sure if unicode icons in a InfoBar feel native
53+
// new InfoBarTextSpan($"NuGet update: 🔼 {outdatedCount} ⚠ {deprecatedCount} 💀 {vulnerableCount}. "),
54+
55+
var textSpans = new List<IVsInfoBarTextSpan>();
56+
57+
if (outdatedCount > 0)
58+
{
59+
textSpans.Add(new InfoBarTextSpan($"{outdatedCount} {(outdatedCount == 1 ? "update" : "updates")} available"));
60+
}
61+
62+
if (deprecatedCount > 0)
63+
{
64+
textSpans.Add(new InfoBarTextSpan($"{(textSpans.Any() ? ", " : string.Empty)}{deprecatedCount} {(deprecatedCount == 1 ? "deprecation" : "deprecations")}"));
65+
}
66+
67+
if (vulnerableCount > 0)
68+
{
69+
textSpans.Add(new InfoBarTextSpan($"{(textSpans.Any() ? ", " : string.Empty)}{vulnerableCount} {(vulnerableCount == 1 ? "vulnerability" : "vulnerabilities")}"));
70+
}
71+
72+
textSpans.Add(new InfoBarTextSpan(". "));
73+
textSpans.Add(new InfoBarHyperlink("Manage"));
74+
textSpans.Add(new InfoBarTextSpan(" packages."));
75+
76+
return textSpans;
3677
}
3778
}
3879
}

Diff for: src/Services/MonitorService.cs

+4-29
Original file line numberDiff line numberDiff line change
@@ -16,38 +16,13 @@ private static void SolutionEvents_OnAfterOpenSolution(Solution solution)
1616
CheckForUpdates().FireAndForget();
1717
}
1818

19-
private static async Task CheckForUpdates()
19+
public static async Task CheckForUpdates()
2020
{
21-
var hasUpdates = await HasUpdates();
21+
var packageReferences = await ProjectService.GetPackageReferences();
2222

23-
if (!hasUpdates)
24-
{
25-
return;
26-
}
23+
packageReferences = await NuGetService.CheckPackageReferences(packageReferences);
2724

28-
await InfoBarService.ShowInfoBar();
29-
}
30-
31-
private static async Task<bool> HasUpdates()
32-
{
33-
var projectPaths = await ProjectService.GetProjectPaths();
34-
35-
foreach (var path in projectPaths)
36-
{
37-
var packageReferences = ProjectService.GetPackageReferences(path);
38-
39-
foreach (var packageReference in packageReferences)
40-
{
41-
var latestVersion = await NuGetService.GetLatestVersion(packageReference.Include);
42-
43-
if (latestVersion > packageReference.Version)
44-
{
45-
return true;
46-
}
47-
}
48-
}
49-
50-
return false;
25+
await InfoBarService.ShowInfoBar(packageReferences);
5126
}
5227
}
5328
}

Diff for: src/Services/NuGetService.cs

+34-6
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,58 @@
11
using NuGet.Common;
22
using NuGet.Protocol;
33
using NuGet.Protocol.Core.Types;
4-
using System;
4+
using System.Collections.Generic;
55
using System.Linq;
66
using System.Threading;
77
using System.Threading.Tasks;
8+
using PackageReference = NuGetMonitor.Models.PackageReference;
89

910
namespace NuGetMonitor.Services
1011
{
1112
public static class NuGetService
1213
{
13-
public static async Task<Version> GetLatestVersion(string id)
14+
public static async Task<List<PackageReference>> CheckPackageReferences(IEnumerable<PackageReference> packageReferences)
15+
{
16+
var result = await Task.WhenAll(
17+
packageReferences
18+
.ToList()
19+
.Select(packageReference => CheckPackageReference(packageReference)));
20+
21+
return result.ToList();
22+
}
23+
24+
private static async Task<PackageReference> CheckPackageReference(PackageReference packageReference)
25+
{
26+
var packageMetadataResource = await Repository.Factory
27+
.GetCoreV3("https://api.nuget.org/v3/index.json")
28+
.GetResourceAsync<PackageMetadataResource>();
29+
30+
var metadata = await packageMetadataResource.GetMetadataAsync(
31+
packageReference.PackageIdentity,
32+
new SourceCacheContext(),
33+
NullLogger.Instance,
34+
CancellationToken.None);
35+
36+
packageReference.IsVulnerable = metadata.Vulnerabilities != null;
37+
packageReference.IsDeprecated = await metadata.GetDeprecationMetadataAsync() != null;
38+
packageReference.IsOutdated = await IsOutdated(packageReference);
39+
40+
return packageReference;
41+
}
42+
43+
private static async Task<bool> IsOutdated(PackageReference packageReference)
1444
{
1545
var packageResource = await Repository.Factory
1646
.GetCoreV3("https://api.nuget.org/v3/index.json")
1747
.GetResourceAsync<FindPackageByIdResource>();
1848

1949
var versions = await packageResource.GetAllVersionsAsync(
20-
id,
50+
packageReference.PackageIdentity.Id,
2151
new SourceCacheContext(),
2252
NullLogger.Instance,
2353
CancellationToken.None);
2454

25-
var latestVersion = versions.Max(version => version.Version);
26-
27-
return latestVersion;
55+
return versions.Last() > packageReference.PackageIdentity.Version;
2856
}
2957
}
3058
}

Diff for: src/Services/ProjectService.cs

+7-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using Community.VisualStudio.Toolkit;
22
using NuGetMonitor.Models;
3-
using System;
43
using System.Collections.Generic;
54
using System.IO;
65
using System.Linq;
@@ -13,26 +12,24 @@ namespace NuGetMonitor.Services
1312
{
1413
public static class ProjectService
1514
{
16-
public static async Task<IEnumerable<string>> GetProjectPaths()
15+
public static async Task<IEnumerable<PackageReference>> GetPackageReferences()
1716
{
1817
var projects = await VS.Solutions.GetAllProjectsAsync();
19-
var projectPaths = projects.Select(project => project.FullPath);
2018

21-
return projectPaths;
19+
return projects
20+
.Select(project => project.FullPath)
21+
.ToList()
22+
.SelectMany(path => GetPackageReferences(path));
2223
}
2324

24-
public static IEnumerable<PackageReference> GetPackageReferences(string projectPath)
25+
private static IEnumerable<PackageReference> GetPackageReferences(string projectPath)
2526
{
2627
var xml = File.ReadAllText(projectPath);
2728
var doc = XDocument.Parse(xml);
2829

2930
var packageReferences = doc
3031
.XPathSelectElements("//PackageReference")
31-
.Select(packageReference => new PackageReference
32-
{
33-
Include = packageReference.Attribute("Include").Value,
34-
Version = new Version(packageReference.Attribute("Version").Value)
35-
});
32+
.Select(packageReference => new PackageReference(packageReference));
3633

3734
return packageReferences;
3835
}

Diff for: src/source.extension.vsixmanifest

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<Metadata>
44
<Identity Id="NuGetMonitor.2a6fbffe-f3fd-4bf8-98cc-5ae2c833a1c7" Version="1.0" Language="en-US" Publisher="Samir Boulema" />
55
<DisplayName>NuGetMonitor</DisplayName>
6-
<Description xml:space="preserve">A Visual Studio extension that checks and notifies about available updates for the installed NuGet packages for the open solution.</Description>
6+
<Description xml:space="preserve">Check and notify about available updates for the installed NuGet packages for the open solution.</Description>
77
<MoreInfo>https://github.com/sboulema/NuGetMonitor</MoreInfo>
88
<License>Resources\LICENSE</License>
99
<GettingStartedGuide>https://github.com/sboulema/NuGetMonitor/blob/main/README.md</GettingStartedGuide>

0 commit comments

Comments
 (0)