From ca76a0734f5c19a62d186b8e358607e975ee7a9a Mon Sep 17 00:00:00 2001 From: nikiforovall Date: Wed, 24 Jul 2024 23:05:48 +0300 Subject: [PATCH 1/7] feat: add serve command --- .vscode/launch.json | 6 ++ dependify.sln | 7 ++ src/Dependify.Cli/Commands/ServeCommand.cs | 49 ++++++++++ src/Dependify.Cli/Dependify.Cli.csproj | 4 +- src/Dependify.Cli/Program.cs | 3 +- src/Directory.Packages.props | 3 +- src/Web/Components/App.razor | 20 ++++ src/Web/Components/Layout/MainLayout.razor | 104 +++++++++++++++++++++ src/Web/Components/Layout/NavMenu.razor | 8 ++ src/Web/Components/Pages/Counter.razor | 18 ++++ src/Web/Components/Pages/Error.razor | 36 +++++++ src/Web/Components/Pages/Home.razor | 58 ++++++++++++ src/Web/Components/Pages/Weather.razor | 60 ++++++++++++ src/Web/Components/Routes.razor | 6 ++ src/Web/Components/_Imports.razor | 12 +++ src/Web/Program.cs | 45 +++++++++ src/Web/Properties/launchSettings.json | 38 ++++++++ src/Web/Web.csproj | 6 ++ src/Web/appsettings.Development.json | 8 ++ src/Web/appsettings.json | 9 ++ src/Web/wwwroot/favicon.ico | 3 + 21 files changed, 500 insertions(+), 3 deletions(-) create mode 100644 src/Dependify.Cli/Commands/ServeCommand.cs create mode 100644 src/Web/Components/App.razor create mode 100644 src/Web/Components/Layout/MainLayout.razor create mode 100644 src/Web/Components/Layout/NavMenu.razor create mode 100644 src/Web/Components/Pages/Counter.razor create mode 100644 src/Web/Components/Pages/Error.razor create mode 100644 src/Web/Components/Pages/Home.razor create mode 100644 src/Web/Components/Pages/Weather.razor create mode 100644 src/Web/Components/Routes.razor create mode 100644 src/Web/Components/_Imports.razor create mode 100644 src/Web/Program.cs create mode 100644 src/Web/Properties/launchSettings.json create mode 100644 src/Web/Web.csproj create mode 100644 src/Web/appsettings.Development.json create mode 100644 src/Web/appsettings.json create mode 100644 src/Web/wwwroot/favicon.ico diff --git a/.vscode/launch.json b/.vscode/launch.json index 1fa2a0e..e32855c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,12 @@ { "version": "0.2.0", "configurations": [ + { + "name": "C#: Web Debug", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/src/Web/Web.csproj", + }, { "name": "debug", "type": "coreclr", diff --git a/dependify.sln b/dependify.sln index 8332e50..8bf20cc 100644 --- a/dependify.sln +++ b/dependify.sln @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{C3712305 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dependify.Core.Tests", "tests\Dependify.Core.Tests\Dependify.Core.Tests.csproj", "{A13ED5C9-227D-4C24-A04C-617A81878415}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web", "src\Web\Web.csproj", "{1358655A-56D9-45B8-80ED-758704415375}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,6 +33,10 @@ Global {A13ED5C9-227D-4C24-A04C-617A81878415}.Debug|Any CPU.Build.0 = Debug|Any CPU {A13ED5C9-227D-4C24-A04C-617A81878415}.Release|Any CPU.ActiveCfg = Release|Any CPU {A13ED5C9-227D-4C24-A04C-617A81878415}.Release|Any CPU.Build.0 = Release|Any CPU + {1358655A-56D9-45B8-80ED-758704415375}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1358655A-56D9-45B8-80ED-758704415375}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1358655A-56D9-45B8-80ED-758704415375}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1358655A-56D9-45B8-80ED-758704415375}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -39,5 +45,6 @@ Global {B9C749D5-B659-45C8-AAA5-D0B6FB99A9D0} = {6EFAF0C3-2695-4C03-AAB0-D982DA582BEB} {C439C106-82A3-4303-A989-09A24E2FF221} = {6EFAF0C3-2695-4C03-AAB0-D982DA582BEB} {A13ED5C9-227D-4C24-A04C-617A81878415} = {C3712305-26BF-4E1B-B7E3-2A603443E98F} + {1358655A-56D9-45B8-80ED-758704415375} = {6EFAF0C3-2695-4C03-AAB0-D982DA582BEB} EndGlobalSection EndGlobal diff --git a/src/Dependify.Cli/Commands/ServeCommand.cs b/src/Dependify.Cli/Commands/ServeCommand.cs new file mode 100644 index 0000000..0aed8dd --- /dev/null +++ b/src/Dependify.Cli/Commands/ServeCommand.cs @@ -0,0 +1,49 @@ +namespace Dependify.Cli.Commands; + +using Dependify.Cli.Commands.Settings; +using Microsoft.Extensions.Logging; + +internal class ServeCommand() : AsyncCommand +{ + public override async Task ExecuteAsync(CommandContext context, ServeCommandSettings settings) + { + var isLoggingEnabled = settings.LogLevel.HasValue && settings.LogLevel.Value != LogLevel.None; + + var taskRun = Web.Program.Run( + new WebApplicationOptions() { }, + webBuilder: builder => + builder.Services.AddLogging(l => + { + l.ClearProviders().AddDebug(); + + if (isLoggingEnabled) + { + l.SetMinimumLevel(settings.LogLevel!.Value); + l.AddSimpleConsole(); + } + }) + ); + + if (!isLoggingEnabled) + { + AnsiConsole.Write(new FigletText("Dependify").LeftJustified().Color(Color.Olive)); + AnsiConsole.MarkupLine( + $"{Environment.NewLine}{Environment.NewLine}Now listening on: [olive]http://localhost:5000[/]{Environment.NewLine}{Environment.NewLine}" + ); + AnsiConsole.MarkupLine("Press [green]Ctrl+C[/] to stop the server"); + } + + await taskRun; + + return 0; + } +} + +internal class ServeCommandSettings : BaseAnalyzeCommandSettings +{ + [CommandOption("--full-scan")] + public bool? FullScan { get; set; } = false; + + [CommandOption("--exclude-sln")] + public bool? ExcludeSln { get; set; } = false; +} diff --git a/src/Dependify.Cli/Dependify.Cli.csproj b/src/Dependify.Cli/Dependify.Cli.csproj index b9081cf..26fa328 100644 --- a/src/Dependify.Cli/Dependify.Cli.csproj +++ b/src/Dependify.Cli/Dependify.Cli.csproj @@ -1,4 +1,4 @@ - + Exe @@ -22,6 +22,7 @@ + @@ -35,6 +36,7 @@ + diff --git a/src/Dependify.Cli/Program.cs b/src/Dependify.Cli/Program.cs index b3cc0a8..fbcfc9f 100644 --- a/src/Dependify.Cli/Program.cs +++ b/src/Dependify.Cli/Program.cs @@ -1,6 +1,5 @@ using Dependify.Cli.Formatters; using Dependify.Core; -using Microsoft.Extensions.Logging; var app = new CommandApp(ConfigureServices(out var configuration)); @@ -20,6 +19,8 @@ } ); + config.AddCommand("serve"); + #if DEBUG config.PropagateExceptions(); config.ValidateExamples(); diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index e438aad..ef97e48 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -16,5 +16,6 @@ + - \ No newline at end of file + diff --git a/src/Web/Components/App.razor b/src/Web/Components/App.razor new file mode 100644 index 0000000..fc42740 --- /dev/null +++ b/src/Web/Components/App.razor @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Web/Components/Layout/MainLayout.razor b/src/Web/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..ae6f235 --- /dev/null +++ b/src/Web/Components/Layout/MainLayout.razor @@ -0,0 +1,104 @@ +@inherits LayoutComponentBase + + + + + + + + + Application + + + + + + + + + @Body + + + + +
+ An unhandled error has occurred. + Reload + 🗙 +
+ +@code { + private bool _drawerOpen = true; + private bool _isDarkMode = true; + private MudTheme? _theme = null; + + protected override void OnInitialized() + { + base.OnInitialized(); + + _theme = new() + { + PaletteLight = _lightPalette, + PaletteDark = _darkPalette, + LayoutProperties = new LayoutProperties() + }; + } + + + private void DrawerToggle() + { + _drawerOpen = !_drawerOpen; + } + + private void DarkModeToggle() + { + _isDarkMode = !_isDarkMode; + } + + private readonly PaletteLight _lightPalette = new() + { + Black = "#110e2d", + AppbarText = "#424242", + AppbarBackground = "rgba(255,255,255,0.8)", + DrawerBackground = "#ffffff", + GrayLight = "#e8e8e8", + GrayLighter = "#f9f9f9", + }; + + private readonly PaletteDark _darkPalette = new() + { + Primary = "#7e6fff", + Surface = "#1e1e2d", + Background = "#1a1a27", + BackgroundGray = "#151521", + AppbarText = "#92929f", + AppbarBackground = "rgba(26,26,39,0.8)", + DrawerBackground = "#1a1a27", + ActionDefault = "#74718e", + ActionDisabled = "#9999994d", + ActionDisabledBackground = "#605f6d4d", + TextPrimary = "#b2b0bf", + TextSecondary = "#92929f", + TextDisabled = "#ffffff33", + DrawerIcon = "#92929f", + DrawerText = "#92929f", + GrayLight = "#2a2833", + GrayLighter = "#1e1e2d", + Info = "#4a86ff", + Success = "#3dcb6c", + Warning = "#ffb545", + Error = "#ff3f5f", + LinesDefault = "#33323e", + TableLines = "#33323e", + Divider = "#292838", + OverlayLight = "#1e1e2d80", + }; + + public string DarkLightModeButtonIcon => _isDarkMode switch + { + true => Icons.Material.Rounded.AutoMode, + false => Icons.Material.Outlined.DarkMode, + }; +} + + diff --git a/src/Web/Components/Layout/NavMenu.razor b/src/Web/Components/Layout/NavMenu.razor new file mode 100644 index 0000000..0225ad0 --- /dev/null +++ b/src/Web/Components/Layout/NavMenu.razor @@ -0,0 +1,8 @@ + + + Home + Counter + Weather + + + diff --git a/src/Web/Components/Pages/Counter.razor b/src/Web/Components/Pages/Counter.razor new file mode 100644 index 0000000..db95bf3 --- /dev/null +++ b/src/Web/Components/Pages/Counter.razor @@ -0,0 +1,18 @@ +@page "/counter" + +Counter + +Counter + +Current count: @currentCount + +Click me + +@code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } +} diff --git a/src/Web/Components/Pages/Error.razor b/src/Web/Components/Pages/Error.razor new file mode 100644 index 0000000..576cc2d --- /dev/null +++ b/src/Web/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/src/Web/Components/Pages/Home.razor b/src/Web/Components/Pages/Home.razor new file mode 100644 index 0000000..2ba687d --- /dev/null +++ b/src/Web/Components/Pages/Home.razor @@ -0,0 +1,58 @@ +@page "/" + +Home + +Hello, world! +Welcome to your new app, powered by MudBlazor and the .NET 8 Template! + + + You can find documentation and examples on our website here: + + www.mudblazor.com + + + +
+Interactivity in this Template +
+ + When you opt for the "Global" Interactivity Location,
+ the render modes are defined in App.razor and consequently apply to all child components.
+ In this case, providers are globally set in the MainLayout.
+
+ On the other hand, if you choose the "Per page/component" Interactivity Location,
+ it is necessary to include the
+
+ <MudPopoverProvider />
+ <MudDialogProvider />
+ <MudSnackbarProvider />
+
+ components on every interactive page.
+
+ If a render mode is not specified for a page, it defaults to Server-Side Rendering (SSR),
+ similar to this page. While MudBlazor allows pages to be rendered in SSR,
+ please note that interactive features, such as buttons and dropdown menus, will not be functional. +
+ +
+What's New in Blazor with the Release of .NET 8 +
+Prerendering + + If you're exploring the features of .NET 8 Blazor,
you might be pleasantly surprised to learn that each page is prerendered on the server,
regardless of the selected render mode.

+ This means that you'll need to inject all necessary services on the server,
even when opting for the wasm (WebAssembly) render mode.

+ This prerendering functionality is crucial to ensuring that WebAssembly mode feels fast and responsive,
especially when it comes to initial page load times.

+ For more information on how to detect prerendering and leverage the RenderContext, you can refer to the following link: + + More details + +
+ +
+InteractiveAuto + + A discussion on how to achieve this can be found here: + + More details + + \ No newline at end of file diff --git a/src/Web/Components/Pages/Weather.razor b/src/Web/Components/Pages/Weather.razor new file mode 100644 index 0000000..3ffd7d8 --- /dev/null +++ b/src/Web/Components/Pages/Weather.razor @@ -0,0 +1,60 @@ +@page "/weather" + + + +Weather + +Weather forecast +This component demonstrates fetching data from the server. + +@if (forecasts == null) +{ + +} +else +{ + + + Date + Temp. (C) + Temp. (F) + Summary + + + @context.Date + @context.TemperatureC + @context.TemperatureF + @context.Summary + + + + + +} + +@code { + private WeatherForecast[]? forecasts; + + protected override async Task OnInitializedAsync() + { + // Simulate asynchronous loading to demonstrate a loading indicator + await Task.Delay(500); + + var startDate = DateOnly.FromDateTime(DateTime.Now); + var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; + forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = startDate.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = summaries[Random.Shared.Next(summaries.Length)] + }).ToArray(); + } + + private class WeatherForecast + { + public DateOnly Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } +} diff --git a/src/Web/Components/Routes.razor b/src/Web/Components/Routes.razor new file mode 100644 index 0000000..f756e19 --- /dev/null +++ b/src/Web/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Web/Components/_Imports.razor b/src/Web/Components/_Imports.razor new file mode 100644 index 0000000..a49f3f9 --- /dev/null +++ b/src/Web/Components/_Imports.razor @@ -0,0 +1,12 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using MudBlazor +@using MudBlazor.Services +@using Web +@using Web.Components diff --git a/src/Web/Program.cs b/src/Web/Program.cs new file mode 100644 index 0000000..c176718 --- /dev/null +++ b/src/Web/Program.cs @@ -0,0 +1,45 @@ +namespace Web; + +using MudBlazor.Services; +using Web.Components; + +public static class Program +{ + public static void Main() => Run().GetAwaiter().GetResult(); + + public static async Task Run( + WebApplicationOptions? appOptions = default, + Action? webBuilder = default + ) + { + appOptions ??= new(); + + var builder = WebApplication.CreateBuilder(appOptions); + + // Add MudBlazor services + builder.Services.AddMudServices(); + + // Add services to the container. + builder.Services.AddRazorComponents().AddInteractiveServerComponents(); + webBuilder?.Invoke(builder); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (!app.Environment.IsDevelopment()) + { + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + app.UseHttpsRedirection(); + + app.UseStaticFiles(); + app.UseAntiforgery(); + + app.MapRazorComponents().AddInteractiveServerRenderMode(); + + await app.RunAsync(); + } +} diff --git a/src/Web/Properties/launchSettings.json b/src/Web/Properties/launchSettings.json new file mode 100644 index 0000000..73bc2e3 --- /dev/null +++ b/src/Web/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:7786", + "sslPort": 44319 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5221", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7168;http://localhost:5221", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj new file mode 100644 index 0000000..a34cb18 --- /dev/null +++ b/src/Web/Web.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Web/appsettings.Development.json b/src/Web/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Web/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Web/appsettings.json b/src/Web/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/Web/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Web/wwwroot/favicon.ico b/src/Web/wwwroot/favicon.ico new file mode 100644 index 0000000..ff12f63 --- /dev/null +++ b/src/Web/wwwroot/favicon.ico @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2435087e2c4ad91f1dec333db7ec11d615ab00190f3c1166a426b59660ec530 +size 15086 From e6df89cba74a47df6f6876800251b16c133b7879 Mon Sep 17 00:00:00 2001 From: nikiforovall Date: Fri, 26 Jul 2024 21:16:03 +0300 Subject: [PATCH 2/7] feat: add MVP serve --- .vscode/launch.json | 27 ++- README.md | 4 + src/Dependify.Cli/Commands/ScanCommand.cs | 9 +- src/Dependify.Cli/Commands/ServeCommand.cs | 130 +++++++++-- src/Dependify.Cli/Commands/ShowCommand.cs | 11 +- src/Dependify.Cli/Dependify.Cli.csproj | 1 + .../Properties/launchSettings.json | 12 + src/Dependify.Cli/Utils.cs | 47 ---- src/Dependify.Cli/wwwroot/favicon.ico | 3 + src/Dependify.Core/Dependify.Core.csproj | 2 + .../FileProviderProjectLocator.cs | 113 +++++++++ src/Dependify.Core/MsBuildService.cs | 25 +- src/Dependify.Core/SolutionRegistry.cs | 86 +++++++ src/Dependify.Core/Utils.cs | 54 ++++- src/Directory.Packages.props | 4 +- src/Web/Components/Layout/MainLayout.razor | 8 +- src/Web/Components/Layout/NavMenu.razor | 2 - src/Web/Components/Pages/Home.razor | 217 +++++++++++++----- src/Web/Components/Pages/Welcome.razor | 58 +++++ src/Web/Program.cs | 5 +- src/Web/Web.csproj | 4 + 21 files changed, 675 insertions(+), 147 deletions(-) create mode 100644 src/Dependify.Cli/Properties/launchSettings.json create mode 100644 src/Dependify.Cli/wwwroot/favicon.ico create mode 100644 src/Dependify.Core/FileProviderProjectLocator.cs create mode 100644 src/Dependify.Core/SolutionRegistry.cs create mode 100644 src/Web/Components/Pages/Welcome.razor diff --git a/.vscode/launch.json b/.vscode/launch.json index e32855c..de90707 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,22 +2,39 @@ "version": "0.2.0", "configurations": [ { - "name": "C#: Web Debug", - "type": "dotnet", + "name": "debug", + "type": "coreclr", "request": "launch", - "projectPath": "${workspaceFolder}/src/Web/Web.csproj", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/Dependify.Cli/bin/Debug/net8.0/Dependify.Cli.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "externalTerminal", + "env": {} }, { - "name": "debug", + "name": "serve:debug", "type": "coreclr", "request": "launch", "preLaunchTask": "build", "program": "${workspaceFolder}/src/Dependify.Cli/bin/Debug/net8.0/Dependify.Cli.dll", - "args": [], + "args": ["serve", "C:\\Users\\Oleksii_Nikiforov\\dev\\dependify\\cap-aspire"], "cwd": "${workspaceFolder}", "stopAtEntry": false, "console": "externalTerminal", "env": {} + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + }, + { + "name": "C#: Web Debug", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/src/Web/Web.csproj" } ] } diff --git a/README.md b/README.md index 57d737c..7da0ab2 100644 --- a/README.md +++ b/README.md @@ -134,3 +134,7 @@ graph LR `dotnet cake --target test` `dotnet cake --target pack` + +`dotnet tool install --global --add-source ./Artefacts Dependify.Cli --prerelease` + +`dotnet tool uninstall Dependify.Cli -g` diff --git a/src/Dependify.Cli/Commands/ScanCommand.cs b/src/Dependify.Cli/Commands/ScanCommand.cs index ca45e8b..e42c4e2 100644 --- a/src/Dependify.Cli/Commands/ScanCommand.cs +++ b/src/Dependify.Cli/Commands/ScanCommand.cs @@ -1,6 +1,5 @@ namespace Dependify.Cli.Commands; -using Dependify.Cli; using Dependify.Cli.Commands.Settings; using Dependify.Cli.Formatters; using Dependify.Core; @@ -41,10 +40,10 @@ public override int Execute(CommandContext context, ScanCommandSettings settings private void DisplayProjects(ScanCommandSettings settings, IEnumerable nodes) { var prefix = Utils.CalculateCommonPrefix(nodes); - var graph = Utils.DoSomeWork( + var graph = Cli.Utils.DoSomeWork( ctx => { - Utils.SetDiagnosticSource(msBuildService, ctx); + Cli.Utils.SetDiagnosticSource(msBuildService, ctx); return msBuildService.AnalyzeReferences( nodes.OfType(), @@ -78,10 +77,10 @@ int solutionCount foreach (var solution in solutionNodes.Where(n => selectedSolutions.Contains(n.Id))) { - var graph = Utils.DoSomeWork( + var graph = Cli.Utils.DoSomeWork( ctx => { - Utils.SetDiagnosticSource(msBuildService, ctx); + Cli.Utils.SetDiagnosticSource(msBuildService, ctx); return msBuildService.AnalyzeReferences( solution, diff --git a/src/Dependify.Cli/Commands/ServeCommand.cs b/src/Dependify.Cli/Commands/ServeCommand.cs index 0aed8dd..4062f95 100644 --- a/src/Dependify.Cli/Commands/ServeCommand.cs +++ b/src/Dependify.Cli/Commands/ServeCommand.cs @@ -1,17 +1,66 @@ namespace Dependify.Cli.Commands; +using System.Threading; using Dependify.Cli.Commands.Settings; +using Dependify.Core; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; internal class ServeCommand() : AsyncCommand { + private const string Port = "9999"; + private const string Host = $"http://localhost:{Port}"; + public override async Task ExecuteAsync(CommandContext context, ServeCommandSettings settings) { var isLoggingEnabled = settings.LogLevel.HasValue && settings.LogLevel.Value != LogLevel.None; - var taskRun = Web.Program.Run( - new WebApplicationOptions() { }, + var directory = Path.GetDirectoryName(settings.Path).NormalizePath(); + + if (!Directory.Exists(directory)) + { + AnsiConsole.MarkupLine($"[red]The specified path does not exist: {directory}[/]"); + + return 1; + } + + await Web.Program.Run( + new WebApplicationOptions { WebRootPath = Path.GetFullPath("./src/Web/wwwroot"), }, webBuilder: builder => + { + builder + .WebHost.UseSetting(WebHostDefaults.ApplicationKey, "Dependify.Cli") + .UseSetting(WebHostDefaults.HttpPortsKey, Port); + + builder.Services.AddSingleton(new PhysicalFileProvider(Path.GetFullPath(directory))); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); + + builder.Services.AddHostedService(sp => new SolutionRegistryService( + sp.GetRequiredService(), + sp.GetRequiredService>(), + new MsBuildServiceListener( + project => { }, + project => + { + if (!isLoggingEnabled) + { + AnsiConsole.MarkupLine($"[green] Loaded: [/] [grey]{project.ProjectFilePath}[/]"); + } + } + ), + isLoggingEnabled + )); + + builder.Services.Configure(config => + { + config.IncludePackages = true; + config.FullScan = true; + config.Framework = settings.Framework; + }); + builder.Services.AddLogging(l => { l.ClearProviders().AddDebug(); @@ -21,29 +70,74 @@ public override async Task ExecuteAsync(CommandContext context, ServeComman l.SetMinimumLevel(settings.LogLevel!.Value); l.AddSimpleConsole(); } - }) - ); + }); + }, + webApp: app => + { + app.Lifetime.ApplicationStarted.Register(() => + { + if (!isLoggingEnabled) + { + AnsiConsole.Write(new FigletText("Dependify").LeftJustified().Color(Color.Olive)); + AnsiConsole.MarkupLine( + $"{Environment.NewLine}{Environment.NewLine}Now listening on: [olive]{Host}[/]{Environment.NewLine}{Environment.NewLine}" + ); + AnsiConsole.MarkupLine( + $"Serving files from: [green]{directory}[/]{Environment.NewLine}{Environment.NewLine}" + ); + AnsiConsole.MarkupLine( + $"Press [yellow]Ctrl+C[/] to stop the server{Environment.NewLine}{Environment.NewLine}" + ); + } + }); - if (!isLoggingEnabled) - { - AnsiConsole.Write(new FigletText("Dependify").LeftJustified().Color(Color.Olive)); - AnsiConsole.MarkupLine( - $"{Environment.NewLine}{Environment.NewLine}Now listening on: [olive]http://localhost:5000[/]{Environment.NewLine}{Environment.NewLine}" - ); - AnsiConsole.MarkupLine("Press [green]Ctrl+C[/] to stop the server"); - } + app.Lifetime.ApplicationStopped.Register(() => + { + if (!isLoggingEnabled) + { + AnsiConsole.WriteLine(); - await taskRun; + var rule = new Rule("End of session...") { Style = Style.Parse("olive dim") }; + AnsiConsole.Write(rule); + } + }); + } + ); return 0; } } -internal class ServeCommandSettings : BaseAnalyzeCommandSettings +internal class ServeCommandSettings : BaseAnalyzeCommandSettings { } + +internal class SolutionRegistryService( + SolutionRegistry solutionRegistry, + IOptions msBuildConfig, + MsBuildServiceListener? listener, + bool isLoggingEnabled +) : BackgroundService { - [CommandOption("--full-scan")] - public bool? FullScan { get; set; } = false; + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + Task.Run(() => + { + solutionRegistry.SetBuildServiceDiagnosticSource(listener); + solutionRegistry.LoadRegistry(); + solutionRegistry.LoadSolutionsAsync(msBuildConfig.Value); + }) + .ContinueWith( + _ => + { + if (!isLoggingEnabled) + { + AnsiConsole.MarkupLine( + $"{Environment.NewLine}[olive]Loaded[/] [grey]{solutionRegistry.Solutions.Count()}[/] [olive]solutions[/]{Environment.NewLine}" + ); + } + }, + stoppingToken + ); - [CommandOption("--exclude-sln")] - public bool? ExcludeSln { get; set; } = false; + return Task.CompletedTask; + } } diff --git a/src/Dependify.Cli/Commands/ShowCommand.cs b/src/Dependify.Cli/Commands/ShowCommand.cs index d328167..2ad0d88 100644 --- a/src/Dependify.Cli/Commands/ShowCommand.cs +++ b/src/Dependify.Cli/Commands/ShowCommand.cs @@ -1,7 +1,6 @@ namespace Dependify.Cli.Commands; using System.ComponentModel; -using Dependify.Cli; using Dependify.Cli.Commands.Settings; using Dependify.Cli.Formatters; using Dependify.Core; @@ -37,7 +36,7 @@ public override int Execute(CommandContext context, ShowCommandSettings settings var selected = SelectNode(settings, nodes, nodesCount); - if (Utils.ShouldOutputTui(settings)) + if (Cli.Utils.ShouldOutputTui(settings)) { AnsiConsole.MarkupLine($"[green] Found: [/] [grey]{selected.Path}[/]"); } @@ -204,10 +203,10 @@ private static DependencyGraph GetGraph(MsBuildService msBuildService, ShowComma { if (selected is SolutionReferenceNode solution) { - return Utils.DoSomeWork( + return Cli.Utils.DoSomeWork( ctx => { - Utils.SetDiagnosticSource(msBuildService, ctx); + Cli.Utils.SetDiagnosticSource(msBuildService, ctx); return msBuildService.AnalyzeReferences( solution, @@ -220,10 +219,10 @@ private static DependencyGraph GetGraph(MsBuildService msBuildService, ShowComma } else if (selected is ProjectReferenceNode project) { - return Utils.DoSomeWork( + return Cli.Utils.DoSomeWork( ctx => { - Utils.SetDiagnosticSource(msBuildService, ctx); + Cli.Utils.SetDiagnosticSource(msBuildService, ctx); return msBuildService.AnalyzeReferences( project, diff --git a/src/Dependify.Cli/Dependify.Cli.csproj b/src/Dependify.Cli/Dependify.Cli.csproj index 26fa328..296b771 100644 --- a/src/Dependify.Cli/Dependify.Cli.csproj +++ b/src/Dependify.Cli/Dependify.Cli.csproj @@ -6,6 +6,7 @@ true dependify dependify + true diff --git a/src/Dependify.Cli/Properties/launchSettings.json b/src/Dependify.Cli/Properties/launchSettings.json new file mode 100644 index 0000000..750157d --- /dev/null +++ b/src/Dependify.Cli/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Development": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } + } diff --git a/src/Dependify.Cli/Utils.cs b/src/Dependify.Cli/Utils.cs index 10535c8..d806f37 100644 --- a/src/Dependify.Cli/Utils.cs +++ b/src/Dependify.Cli/Utils.cs @@ -2,42 +2,11 @@ namespace Dependify.Cli; using Dependify.Cli.Commands.Settings; using Dependify.Core; -using Dependify.Core.Graph; -using Depends.Core.Graph; using Microsoft.Extensions.Logging; internal static class Utils { - public static string RemovePrefix(this string value, string prefix) - { - if (value.StartsWith(prefix, StringComparison.InvariantCulture)) - { - return value[prefix.Length..].TrimStart('/', '\\'); - } - - return value; - } - - public static string GetFullPath(string pathArg) - { - FileSystemInfo fileSystemInfo; - - if (File.Exists(pathArg)) - { - fileSystemInfo = new FileInfo(pathArg); - } - else if (Directory.Exists(pathArg)) - { - fileSystemInfo = new DirectoryInfo(pathArg); - } - else - { - throw new ArgumentException("The specified path does not exist.", nameof(pathArg)); - } - var path = fileSystemInfo.FullName; - return path; - } public static bool ShouldOutputTui(GlobalCommandSettings settings) => (settings.LogLevel is LogLevel.None && settings.Format is OutputFormat.Tui) @@ -84,21 +53,5 @@ public static void SetDiagnosticSource(MsBuildService msBuildService, StatusCont ) ); - public static string CalculateCommonPrefix(IEnumerable nodes) - { - var prefix = nodes - .OfType() - .Select(n => n.Path) - .Aggregate( - (a, b) => - a.Zip(b) - .TakeWhile(p => p.First == p.Second) - .Select(p => p.First) - .Aggregate(string.Empty, (a, b) => a + b) - ); - - prefix = prefix[..prefix.LastIndexOfAny(['/', '\\'])]; - return prefix; - } } diff --git a/src/Dependify.Cli/wwwroot/favicon.ico b/src/Dependify.Cli/wwwroot/favicon.ico new file mode 100644 index 0000000..ff12f63 --- /dev/null +++ b/src/Dependify.Cli/wwwroot/favicon.ico @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2435087e2c4ad91f1dec333db7ec11d615ab00190f3c1166a426b59660ec530 +size 15086 diff --git a/src/Dependify.Core/Dependify.Core.csproj b/src/Dependify.Core/Dependify.Core.csproj index 29e4767..7e7a32c 100644 --- a/src/Dependify.Core/Dependify.Core.csproj +++ b/src/Dependify.Core/Dependify.Core.csproj @@ -1,6 +1,8 @@  + + diff --git a/src/Dependify.Core/FileProviderProjectLocator.cs b/src/Dependify.Core/FileProviderProjectLocator.cs new file mode 100644 index 0000000..941f6e7 --- /dev/null +++ b/src/Dependify.Core/FileProviderProjectLocator.cs @@ -0,0 +1,113 @@ +namespace Dependify.Core; + +using Dependify.Core.Graph; +using Depends.Core.Graph; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; + +public class FileProviderProjectLocator(IFileProvider fileProvider, ILogger logger) +{ + private const string SolutionFileExtension = ".sln"; + private const string ProjectFileExtension = ".csproj"; + + public string Root => + (fileProvider as PhysicalFileProvider)?.Root + ?? throw new InvalidOperationException("File provider is not a physical file provider."); + + /// + /// Scans the specified path for .csproj and solution files. + /// + /// + public IEnumerable FullScan() + { + return this.Scan(); + } + + /// + /// Scans the specified path for .csproj and solution files. + /// + /// + public IEnumerable FolderScan() + { + return this.Scan(1); + } + + private IEnumerable Scan(int maxDepth = -1) + { + IEnumerable result = []; + + var files = fileProvider.FindFiles( + string.Empty, + f => + f.Name.EndsWith(ProjectFileExtension, StringComparison.OrdinalIgnoreCase) + || f.Name.EndsWith(SolutionFileExtension, StringComparison.OrdinalIgnoreCase), + maxDepth + ); + + foreach (var file in files) + { + if (file.Name.EndsWith(ProjectFileExtension, StringComparison.OrdinalIgnoreCase)) + { + result = result.Append(new ProjectReferenceNode(file.PhysicalPath!)); + } + else if (file.Name.EndsWith(SolutionFileExtension, StringComparison.OrdinalIgnoreCase)) + { + result = result.Append(new SolutionReferenceNode(file.PhysicalPath)); + } + } + + logger.LogInformation("Located number of items - {Count}", result.Count()); + + foreach (var item in result) + { + logger.LogDebug("Located item - {Item}", item); + } + + return result; + } +} + +public static class FileProviderExtensions +{ + public static IEnumerable FindFiles( + this IFileProvider provider, + string directory, + Predicate match, + int maxDepth = -1 + ) + { + var dirsToSearch = new Stack(); + dirsToSearch.Push(directory); + + var depth = 0; + + while (dirsToSearch.Count > 0) + { + var dir = dirsToSearch.Pop(); + foreach (var file in provider.GetDirectoryContents(dir)) + { + if (file.IsDirectory) + { + if (maxDepth != -1 && depth >= maxDepth) + { + continue; + } + + var relPath = Path.Join(dir, file.Name); + dirsToSearch.Push(relPath); + } + else + { + if (!match(file)) + { + continue; + } + + yield return file; + } + } + + depth++; + } + } +} diff --git a/src/Dependify.Core/MsBuildService.cs b/src/Dependify.Core/MsBuildService.cs index e1861d3..36620eb 100644 --- a/src/Dependify.Core/MsBuildService.cs +++ b/src/Dependify.Core/MsBuildService.cs @@ -6,13 +6,34 @@ namespace Dependify.Core; using Microsoft.Build.Construction; using Microsoft.Extensions.Logging; -public record MsBuildConfig(bool IncludePackages = false, bool FullScan = false, string? Framework = default); +public record MsBuildConfig +{ + public MsBuildConfig() { } + + public MsBuildConfig(bool includePackages, bool fullScan, string? framework) + { + this.IncludePackages = includePackages; + this.FullScan = fullScan; + this.Framework = framework; + } + + public bool IncludePackages { get; set; } + public bool FullScan { get; set; } + public string? Framework { get; set; } + + public void Deconstruct(out bool includePackages, out bool fullScan, out string? framework) + { + includePackages = this.IncludePackages; + fullScan = this.FullScan; + framework = this.Framework; + } +} public class MsBuildService(ILogger logger, ILoggerFactory loggerFactory) { private MsBuildServiceListener? listener; - public void SetDiagnosticSource(MsBuildServiceListener listener) + public void SetDiagnosticSource(MsBuildServiceListener? listener) { this.listener = listener; } diff --git a/src/Dependify.Core/SolutionRegistry.cs b/src/Dependify.Core/SolutionRegistry.cs new file mode 100644 index 0000000..281f555 --- /dev/null +++ b/src/Dependify.Core/SolutionRegistry.cs @@ -0,0 +1,86 @@ +namespace Dependify.Core; + +using Dependify.Core.Graph; +using Depends.Core.Graph; + +public class SolutionRegistry(FileProviderProjectLocator projectLocator, MsBuildService buildService) +{ + private readonly Dictionary solutionGraphs = []; + private static readonly object LockObject = new(); + + private readonly FileProviderProjectLocator projectLocator = projectLocator; + private readonly MsBuildService buildService = buildService; + + public void LoadRegistry() + { + var nodes = this.projectLocator.FullScan().ToList(); + + this.Solutions = nodes.OfType().ToList(); + this.Nodes = nodes; + } + + public Task LoadSolutionsAsync(MsBuildConfig msBuildConfig, CancellationToken cancellationToken = default) + { + this.buildService.SetDiagnosticSource(this.listener); + + lock (LockObject) + { + for (var i = 0; i < this.Solutions.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + var solution = this.Solutions[i]; + + var dependencyGraph = this.buildService.AnalyzeReferences(solution, msBuildConfig); + + // TODO: add cache lookup for already loaded solutions + this.solutionGraphs.Add(solution, dependencyGraph); + + this.solutionRegistryListener?.SolutionLoaded?.Invoke(solution, solution == this.Solutions[^1]); + } + + this.IsLoaded = true; + } + + return Task.CompletedTask; + } + + public NodeUsageStatistics GetDependencyCount(SolutionReferenceNode solution, Node node) + { + var graph = this.GetGraph(solution); + + return graph is null + ? new(node, 0, 0, 0) + : new( + node, + graph.FindDescendants(node).OfType().Count(), + graph.FindDescendants(node).OfType().Count(), + graph.FindAscendants(node).OfType().Count() + ); + } + + public record NodeUsageStatistics(Node Node, int DependsOnProjects, int DependsOnPackages, int UsedBy); + public IList Solutions { get; private set; } = []; + public IList Nodes { get; private set; } + public bool IsLoaded { get; private set; } + + public DependencyGraph? GetGraph(SolutionReferenceNode solution) + { + return this.solutionGraphs.TryGetValue(solution, out var graph) ? graph : null; + } + + private MsBuildServiceListener? listener; + private SolutionRegistryListener? solutionRegistryListener; + + public void SetBuildServiceDiagnosticSource(MsBuildServiceListener? listener = default) + { + this.listener = listener; + } + + public void SetSolutionRegistryListener(SolutionRegistryListener? listener = default) + { + this.solutionRegistryListener = listener; + } +} + +public record SolutionRegistryListener(Action? SolutionLoaded); diff --git a/src/Dependify.Core/Utils.cs b/src/Dependify.Core/Utils.cs index 7ba784d..af8a623 100644 --- a/src/Dependify.Core/Utils.cs +++ b/src/Dependify.Core/Utils.cs @@ -1,6 +1,58 @@ namespace Dependify.Core; -internal static class Utils +using Dependify.Core.Graph; +using Depends.Core.Graph; + +public static class Utils { public static string NormalizePath(this string path) => path.Replace('\\', '/'); + + public static string RemovePrefix(this string value, string prefix) + { + if (value.StartsWith(prefix, StringComparison.InvariantCulture)) + { + return value[prefix.Length..].TrimStart('/', '\\'); + } + + return value; + } + + public static string CalculateCommonPrefix(IEnumerable nodes) + { + var prefix = nodes + .OfType() + .Select(n => n.Path) + .Aggregate( + (a, b) => + a.Zip(b) + .TakeWhile(p => p.First == p.Second) + .Select(p => p.First) + .Aggregate(string.Empty, (a, b) => a + b) + ); + + prefix = prefix[..prefix.LastIndexOfAny(['/', '\\'])]; + + return prefix; + } + + public static string GetFullPath(string pathArg) + { + FileSystemInfo fileSystemInfo; + + if (File.Exists(pathArg)) + { + fileSystemInfo = new FileInfo(pathArg); + } + else if (Directory.Exists(pathArg)) + { + fileSystemInfo = new DirectoryInfo(pathArg); + } + else + { + throw new ArgumentException("The specified path does not exist.", nameof(pathArg)); + } + + var path = fileSystemInfo.FullName; + return path; + } } diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index ef97e48..2e798cb 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -10,6 +10,8 @@ + + @@ -18,4 +20,4 @@ - + \ No newline at end of file diff --git a/src/Web/Components/Layout/MainLayout.razor b/src/Web/Components/Layout/MainLayout.razor index ae6f235..6ad2a40 100644 --- a/src/Web/Components/Layout/MainLayout.razor +++ b/src/Web/Components/Layout/MainLayout.razor @@ -7,10 +7,12 @@ - Application + Dependify + @@ -28,8 +30,8 @@ @code { - private bool _drawerOpen = true; - private bool _isDarkMode = true; + private bool _drawerOpen = false; + private bool _isDarkMode = false; private MudTheme? _theme = null; protected override void OnInitialized() diff --git a/src/Web/Components/Layout/NavMenu.razor b/src/Web/Components/Layout/NavMenu.razor index 0225ad0..0dc3c32 100644 --- a/src/Web/Components/Layout/NavMenu.razor +++ b/src/Web/Components/Layout/NavMenu.razor @@ -1,8 +1,6 @@  Home - Counter - Weather diff --git a/src/Web/Components/Pages/Home.razor b/src/Web/Components/Pages/Home.razor index 2ba687d..68149e5 100644 --- a/src/Web/Components/Pages/Home.razor +++ b/src/Web/Components/Pages/Home.razor @@ -1,58 +1,163 @@ @page "/" +@using Dependify.Core +@using Dependify.Core.Graph +@using Depends.Core.Graph +@using Microsoft.Extensions.Options +@using static Dependify.Core.SolutionRegistry -Home - -Hello, world! -Welcome to your new app, powered by MudBlazor and the .NET 8 Template! - - - You can find documentation and examples on our website here: - - www.mudblazor.com - - - -
-Interactivity in this Template -
- - When you opt for the "Global" Interactivity Location,
- the render modes are defined in App.razor and consequently apply to all child components.
- In this case, providers are globally set in the MainLayout.
-
- On the other hand, if you choose the "Per page/component" Interactivity Location,
- it is necessary to include the
-
- <MudPopoverProvider />
- <MudDialogProvider />
- <MudSnackbarProvider />
-
- components on every interactive page.
-
- If a render mode is not specified for a page, it defaults to Server-Side Rendering (SSR),
- similar to this page. While MudBlazor allows pages to be rendered in SSR,
- please note that interactive features, such as buttons and dropdown menus, will not be functional. -
- -
-What's New in Blazor with the Release of .NET 8 -
-Prerendering - - If you're exploring the features of .NET 8 Blazor,
you might be pleasantly surprised to learn that each page is prerendered on the server,
regardless of the selected render mode.

- This means that you'll need to inject all necessary services on the server,
even when opting for the wasm (WebAssembly) render mode.

- This prerendering functionality is crucial to ensuring that WebAssembly mode feels fast and responsive,
especially when it comes to initial page load times.

- For more information on how to detect prerendering and leverage the RenderContext, you can refer to the following link: - - More details - -
- -
-InteractiveAuto - - A discussion on how to achieve this can be found here: - - More details - - \ No newline at end of file +@inject SolutionRegistry SolutionRegistry +@inject ISnackbar Snackbar +@inject IOptions MsBuildConfig + +Dependify + + + Workbench ⚙️ + + + + @foreach (var solutionNode in solutionNodes) + { + + } + + + + + + @if (displayMode == DisplayMode.All) + { + + + + Name + Path + + + + @foreach (var node in nodes) + { + + @if (node.Type == "Solution") + { + @node.Id + } + else + { + @node.Id + } + @node.DirectoryPath.RemovePrefix(commonPrefix) + + } + + + } + else + { + + + + Name + Depends On Projects + Used By Projects + Depends On Packages + + + + @foreach (var project in projects) + { + + @if (project.Node.Type == "Solution") + { + @project.Node.Id + } + else + { + @project.Node.Id + } + @project.DependsOnProjects + @project.UsedBy + @project.DependsOnPackages + + } + + + } + + + +@code { + private List nodes = []; + + private List solutionNodes = []; + + private string? selectedSolution; + + private List projects = []; + + private string? commonPrefix; + + private DisplayMode displayMode = DisplayMode.All; + + protected override Task OnInitializedAsync() + { + SolutionRegistry.SetSolutionRegistryListener(new((solution, finish) => { + Snackbar.Add($"Loaded - {solution.Id}", Severity.Success); + + if(finish) + { + Snackbar.Add($"Loaded all solutions", Severity.Success); + } + })); + + Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomRight; + + nodes = SolutionRegistry.Nodes.ToList(); + + commonPrefix = Utils.CalculateCommonPrefix(nodes); + + solutionNodes = SolutionRegistry.Solutions.ToList(); + + selectedSolution = SolutionRegistry.Solutions.FirstOrDefault()?.Id; + + return Task.CompletedTask; + } + + private void AnalyzeSolution() + { + var solution = solutionNodes.FirstOrDefault(n => n.Id == selectedSolution); + + if(solution is null) + { + Snackbar.Add($"Select the solution to analyze", Severity.Error); + } + else if(SolutionRegistry.GetGraph(solution) is var graph && graph is not null) + { + projects = graph + .Nodes + .OfType() + .Select(n => SolutionRegistry.GetDependencyCount(solution, n)).ToList(); + + displayMode = DisplayMode.Solutions; + } + else + { + Snackbar.Add($"Analyzing - {selectedSolution}, please wait...", Severity.Normal); + } + } + + private void SetDiagnosticSource(MsBuildService msBuildService) => + msBuildService.SetDiagnosticSource( + new( + project => Snackbar.Add($"Loading - {project.Id}", Severity.Normal), + project => Snackbar.Add($"Loaded - {project.ProjectFilePath}", Severity.Success) + ) + ); + + private enum DisplayMode + { + All, + Solutions, + } +} diff --git a/src/Web/Components/Pages/Welcome.razor b/src/Web/Components/Pages/Welcome.razor new file mode 100644 index 0000000..9823e1c --- /dev/null +++ b/src/Web/Components/Pages/Welcome.razor @@ -0,0 +1,58 @@ +@page "/welcome" + +Home + +Hello, world! +Welcome to your new app, powered by MudBlazor and the .NET 8 Template! + + + You can find documentation and examples on our website here: + + www.mudblazor.com + + + +
+Interactivity in this Template +
+ + When you opt for the "Global" Interactivity Location,
+ the render modes are defined in App.razor and consequently apply to all child components.
+ In this case, providers are globally set in the MainLayout.
+
+ On the other hand, if you choose the "Per page/component" Interactivity Location,
+ it is necessary to include the
+
+ <MudPopoverProvider />
+ <MudDialogProvider />
+ <MudSnackbarProvider />
+
+ components on every interactive page.
+
+ If a render mode is not specified for a page, it defaults to Server-Side Rendering (SSR),
+ similar to this page. While MudBlazor allows pages to be rendered in SSR,
+ please note that interactive features, such as buttons and dropdown menus, will not be functional. +
+ +
+What's New in Blazor with the Release of .NET 8 +
+Prerendering + + If you're exploring the features of .NET 8 Blazor,
you might be pleasantly surprised to learn that each page is prerendered on the server,
regardless of the selected render mode.

+ This means that you'll need to inject all necessary services on the server,
even when opting for the wasm (WebAssembly) render mode.

+ This prerendering functionality is crucial to ensuring that WebAssembly mode feels fast and responsive,
especially when it comes to initial page load times.

+ For more information on how to detect prerendering and leverage the RenderContext, you can refer to the following link: + + More details + +
+ +
+InteractiveAuto + + A discussion on how to achieve this can be found here: + + More details + + diff --git a/src/Web/Program.cs b/src/Web/Program.cs index c176718..7953fe9 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -9,7 +9,8 @@ public static class Program public static async Task Run( WebApplicationOptions? appOptions = default, - Action? webBuilder = default + Action? webBuilder = default, + Action? webApp = default ) { appOptions ??= new(); @@ -40,6 +41,8 @@ public static async Task Run( app.MapRazorComponents().AddInteractiveServerRenderMode(); + webApp?.Invoke(app); + await app.RunAsync(); } } diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index a34cb18..318be52 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -3,4 +3,8 @@ + + + + From 193e6d9869318f21ba5a8f2a2829342082cf64ea Mon Sep 17 00:00:00 2001 From: nikiforovall Date: Sat, 27 Jul 2024 10:19:02 +0300 Subject: [PATCH 3/7] feat: add mermaid graph --- src/Dependify.Cli/Commands/ServeCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dependify.Cli/Commands/ServeCommand.cs b/src/Dependify.Cli/Commands/ServeCommand.cs index 4062f95..7024a70 100644 --- a/src/Dependify.Cli/Commands/ServeCommand.cs +++ b/src/Dependify.Cli/Commands/ServeCommand.cs @@ -16,7 +16,7 @@ public override async Task ExecuteAsync(CommandContext context, ServeComman { var isLoggingEnabled = settings.LogLevel.HasValue && settings.LogLevel.Value != LogLevel.None; - var directory = Path.GetDirectoryName(settings.Path).NormalizePath(); + var directory = Path.GetDirectoryName($"{settings.Path.TrimEnd('/')}/").NormalizePath(); if (!Directory.Exists(directory)) { From 5497bf99d4dcda93deaa970e97c7834de57f1ad7 Mon Sep 17 00:00:00 2001 From: nikiforovall Date: Sat, 27 Jul 2024 10:25:46 +0300 Subject: [PATCH 4/7] feat: add mermaid --- src/Dependify.Core/Graph/DependencyGraph.cs | 27 ++++ src/Dependify.Core/MsBuildService.cs | 1 + src/Dependify.Core/SolutionRegistry.cs | 21 +++- src/Web/Components/App.razor | 5 +- src/Web/Components/Pages/DiagramModal.razor | 27 ++++ src/Web/Components/Pages/Home.razor | 133 +++++++++++++++++--- 6 files changed, 191 insertions(+), 23 deletions(-) create mode 100644 src/Web/Components/Pages/DiagramModal.razor diff --git a/src/Dependify.Core/Graph/DependencyGraph.cs b/src/Dependify.Core/Graph/DependencyGraph.cs index 62dfe11..029aaa7 100644 --- a/src/Dependify.Core/Graph/DependencyGraph.cs +++ b/src/Dependify.Core/Graph/DependencyGraph.cs @@ -27,6 +27,33 @@ public IEnumerable FindAscendants(Node node) return this.Edges.Where(edge => edge.End == node).Select(edge => edge.Start).Distinct(); } + public DependencyGraph SubGraph(Node node, Func? filter = default) + { + var nodes = this.FindAllDescendants(node, filter).Concat([node]).ToList(); + + var edges = this.Edges.Where(edge => nodes.Contains(edge.Start) && filter?.Invoke(edge.End) == true).ToList(); + + return new DependencyGraph(node, nodes, edges); + } + + private IEnumerable FindAllDescendants(Node node, Func? filter = default) + { + var nodes = new List(); + + foreach (var child in this.FindDescendants(node)) + { + if (filter is not null && !filter(child)) + { + continue; + } + + nodes.Add(child); + nodes.AddRange(this.FindAllDescendants(child, filter)); + } + + return nodes; + } + public DependencyGraph CopyNoRoot() { return new DependencyGraph( diff --git a/src/Dependify.Core/MsBuildService.cs b/src/Dependify.Core/MsBuildService.cs index 36620eb..7120e1c 100644 --- a/src/Dependify.Core/MsBuildService.cs +++ b/src/Dependify.Core/MsBuildService.cs @@ -150,6 +150,7 @@ MsBuildConfig config _ = analyzerResult ?? throw new InvalidOperationException("Unable to load project."); this.listener?.OnProjectLoaded?.Invoke(analyzerResult); + builder.WithNode(projectNode, true); foreach (var reference in analyzerResult.ProjectReferences) diff --git a/src/Dependify.Core/SolutionRegistry.cs b/src/Dependify.Core/SolutionRegistry.cs index 281f555..d7ff637 100644 --- a/src/Dependify.Core/SolutionRegistry.cs +++ b/src/Dependify.Core/SolutionRegistry.cs @@ -50,16 +50,15 @@ public NodeUsageStatistics GetDependencyCount(SolutionReferenceNode solution, No var graph = this.GetGraph(solution); return graph is null - ? new(node, 0, 0, 0) + ? new(node, [], [], []) : new( node, - graph.FindDescendants(node).OfType().Count(), - graph.FindDescendants(node).OfType().Count(), - graph.FindAscendants(node).OfType().Count() + graph.FindDescendants(node).OfType().ToList(), + graph.FindDescendants(node).OfType().ToList(), + graph.FindAscendants(node).OfType().ToList() ); } - public record NodeUsageStatistics(Node Node, int DependsOnProjects, int DependsOnPackages, int UsedBy); public IList Solutions { get; private set; } = []; public IList Nodes { get; private set; } public bool IsLoaded { get; private set; } @@ -84,3 +83,15 @@ public void SetSolutionRegistryListener(SolutionRegistryListener? listener = def } public record SolutionRegistryListener(Action? SolutionLoaded); + +public record NodeUsageStatistics( + Node Node, + IList DependsOnProjects, + IList DependsOnPackages, + IList UsedBy +) +{ + public int DependsOnProjectsCount => this.DependsOnProjects.Count; + public int DependsOnPackagesCount => this.DependsOnPackages.Count; + public int UsedByCount => this.UsedBy.Count; +} diff --git a/src/Web/Components/App.razor b/src/Web/Components/App.razor index fc42740..e45dde7 100644 --- a/src/Web/Components/App.razor +++ b/src/Web/Components/App.razor @@ -4,9 +4,9 @@ - + - + @@ -15,6 +15,7 @@ + diff --git a/src/Web/Components/Pages/DiagramModal.razor b/src/Web/Components/Pages/DiagramModal.razor new file mode 100644 index 0000000..7d29841 --- /dev/null +++ b/src/Web/Components/Pages/DiagramModal.razor @@ -0,0 +1,27 @@ +@inject IJSRuntime JSRuntime + +@code { + [CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!; + [Parameter] public string DiagramContent { get; set; } = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await JSRuntime.InvokeVoidAsync("mermaid.init"); + } + } + + private void Close() => MudDialog.Close(); +} + + + +
+ @DiagramContent +
+
+ + Close + +
diff --git a/src/Web/Components/Pages/Home.razor b/src/Web/Components/Pages/Home.razor index 68149e5..3b3324a 100644 --- a/src/Web/Components/Pages/Home.razor +++ b/src/Web/Components/Pages/Home.razor @@ -1,6 +1,7 @@ @page "/" @using Dependify.Core @using Dependify.Core.Graph +@using Dependify.Core.Serializers @using Depends.Core.Graph @using Microsoft.Extensions.Options @using static Dependify.Core.SolutionRegistry @@ -8,6 +9,7 @@ @inject SolutionRegistry SolutionRegistry @inject ISnackbar Snackbar @inject IOptions MsBuildConfig +@inject IDialogService DialogService Dependify @@ -25,7 +27,7 @@ - + @if (displayMode == DisplayMode.All) { @@ -58,27 +60,74 @@ - Name - Depends On Projects - Used By Projects - Depends On Packages + Name + Depends On Projects + Used By Projects + Depends On Packages + Actions @foreach (var project in projects) { - @if (project.Node.Type == "Solution") - { - @project.Node.Id - } - else - { - @project.Node.Id - } - @project.DependsOnProjects - @project.UsedBy - @project.DependsOnPackages + + + + + @project.Node.Id + + + @project.Node.DirectoryPath + + + + + + + @project.DependsOnProjectsCount + + + @foreach (var childProject in project.DependsOnProjects) + { + @childProject.Id + } + + + + + + + @project.UsedByCount + + + @foreach (var childProject in project.UsedBy) + { + @childProject.Id + } + + + + + + + + @project.DependsOnPackagesCount + + + @foreach (var package in project.DependsOnPackages) + { + @package.Id [@package.Version] + } + + + + + + + + + } @@ -147,6 +196,58 @@ } } + private async Task ShowDiagramModal(string nodeId) + { + var solution = solutionNodes.FirstOrDefault(n => n.Id == selectedSolution); + + var subGraph = GetSubGraph(nodeId); + + if(subGraph is null) + { + return; + } + + var diagramContent = MermaidSerializer.ToString(subGraph); + var parameters = new DialogParameters { ["DiagramContent"] = diagramContent }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraLarge, FullWidth = true }; + + var dialog = DialogService.Show("Mermaid Diagram", parameters, options); + + await dialog.Result; + } + + private async Task CopyDiagramToClipboard(string nodeId) + { + var solution = solutionNodes.FirstOrDefault(n => n.Id == selectedSolution); + + var subGraph = GetSubGraph(nodeId); + + if(subGraph is null) + { + return; + } + + Snackbar.Add($"Copied to clipboard", Severity.Normal); + } + + private DependencyGraph? GetSubGraph(string nodeId) + { + var solution = solutionNodes.FirstOrDefault(n => n.Id == selectedSolution); + + if(SolutionRegistry.GetGraph(solution) is var graph && graph is not null) + { + var project = graph.Nodes.FirstOrDefault(n => n.Id == nodeId); + + displayMode = DisplayMode.Solutions; + + var subGraph = graph.SubGraph(project, n => n.Type is not "Package"); + + return subGraph; + } + + return default; + } + private void SetDiagnosticSource(MsBuildService msBuildService) => msBuildService.SetDiagnosticSource( new( From 9e87151163ebbcfe6d210cea1db94cddd8e0e384 Mon Sep 17 00:00:00 2001 From: nikiforovall Date: Sun, 28 Jul 2024 10:35:53 +0300 Subject: [PATCH 5/7] feat: add mudblazor cdn --- src/Dependify.Cli/Commands/ServeCommand.cs | 1 - src/Dependify.Cli/Dependify.Cli.csproj | 1 - src/Dependify.Cli/wwwroot/favicon.ico | 4 ++-- src/Web/Components/App.razor | 4 ++-- src/Web/Components/Pages/Home.razor | 3 +++ src/Web/Web.csproj | 3 +++ 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Dependify.Cli/Commands/ServeCommand.cs b/src/Dependify.Cli/Commands/ServeCommand.cs index 7024a70..a927496 100644 --- a/src/Dependify.Cli/Commands/ServeCommand.cs +++ b/src/Dependify.Cli/Commands/ServeCommand.cs @@ -26,7 +26,6 @@ public override async Task ExecuteAsync(CommandContext context, ServeComman } await Web.Program.Run( - new WebApplicationOptions { WebRootPath = Path.GetFullPath("./src/Web/wwwroot"), }, webBuilder: builder => { builder diff --git a/src/Dependify.Cli/Dependify.Cli.csproj b/src/Dependify.Cli/Dependify.Cli.csproj index 296b771..f08b3eb 100644 --- a/src/Dependify.Cli/Dependify.Cli.csproj +++ b/src/Dependify.Cli/Dependify.Cli.csproj @@ -23,7 +23,6 @@ - diff --git a/src/Dependify.Cli/wwwroot/favicon.ico b/src/Dependify.Cli/wwwroot/favicon.ico index ff12f63..0c05df7 100644 --- a/src/Dependify.Cli/wwwroot/favicon.ico +++ b/src/Dependify.Cli/wwwroot/favicon.ico @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2435087e2c4ad91f1dec333db7ec11d615ab00190f3c1166a426b59660ec530 -size 15086 +oid sha256:470f7508899e0c33add76c10409c6dab6fa2eb1ce3957bdd87d752093eab27ef +size 15406 diff --git a/src/Web/Components/App.razor b/src/Web/Components/App.razor index e45dde7..f35e759 100644 --- a/src/Web/Components/App.razor +++ b/src/Web/Components/App.razor @@ -6,7 +6,7 @@ - + @@ -14,7 +14,7 @@ - + diff --git a/src/Web/Components/Pages/Home.razor b/src/Web/Components/Pages/Home.razor index 3b3324a..d2b0bab 100644 --- a/src/Web/Components/Pages/Home.razor +++ b/src/Web/Components/Pages/Home.razor @@ -10,6 +10,7 @@ @inject ISnackbar Snackbar @inject IOptions MsBuildConfig @inject IDialogService DialogService +@inject IJSRuntime JSRuntime Dependify @@ -228,6 +229,8 @@ } Snackbar.Add($"Copied to clipboard", Severity.Normal); + + await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", MermaidSerializer.ToString(subGraph)); } private DependencyGraph? GetSubGraph(string nodeId) diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index 318be52..02a9fba 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -1,4 +1,7 @@ + + false + From 97bf96edb2873919afc28605b01d33f0a69ffd99 Mon Sep 17 00:00:00 2001 From: nikiforovall Date: Sun, 28 Jul 2024 17:39:17 +0300 Subject: [PATCH 6/7] feat: add scroll --- src/Web/Components/App.razor | 10 +++++ src/Web/Components/Layout/MainLayout.razor | 1 - src/Web/Components/Pages/DiagramModal.razor | 15 +++++-- src/Web/Components/Pages/Home.razor | 48 ++++++++++++++------- 4 files changed, 54 insertions(+), 20 deletions(-) diff --git a/src/Web/Components/App.razor b/src/Web/Components/App.razor index f35e759..7784910 100644 --- a/src/Web/Components/App.razor +++ b/src/Web/Components/App.razor @@ -7,6 +7,7 @@ + @@ -16,6 +17,15 @@ + + diff --git a/src/Web/Components/Layout/MainLayout.razor b/src/Web/Components/Layout/MainLayout.razor index 6ad2a40..d9fdd41 100644 --- a/src/Web/Components/Layout/MainLayout.razor +++ b/src/Web/Components/Layout/MainLayout.razor @@ -10,7 +10,6 @@ Dependify - diff --git a/src/Web/Components/Pages/DiagramModal.razor b/src/Web/Components/Pages/DiagramModal.razor index 7d29841..d4ac792 100644 --- a/src/Web/Components/Pages/DiagramModal.razor +++ b/src/Web/Components/Pages/DiagramModal.razor @@ -10,6 +10,7 @@ { await JSRuntime.InvokeVoidAsync("mermaid.init"); } + await JSRuntime.InvokeVoidAsync("initializeZoomist"); } private void Close() => MudDialog.Close(); @@ -17,9 +18,17 @@ -
- @DiagramContent -
+ +
+
+
+
+ @DiagramContent +
+
+
+
+
Close diff --git a/src/Web/Components/Pages/Home.razor b/src/Web/Components/Pages/Home.razor index d2b0bab..fb0a849 100644 --- a/src/Web/Components/Pages/Home.razor +++ b/src/Web/Components/Pages/Home.razor @@ -42,14 +42,16 @@ @foreach (var node in nodes) { + @if (node.Type == "Solution") { - @node.Id + @node.Id } else { - @node.Id + @node.Id } + @node.DirectoryPath.RemovePrefix(commonPrefix) } @@ -69,27 +71,34 @@ - @foreach (var project in projects) + @foreach (var nodeUsage in nodeUsageStatistics) { - @project.Node.Id + @if (nodeUsage.Node.Type == "Solution") + { + @nodeUsage.Node.Id + } + else + { + @nodeUsage.Node.Id + } - @project.Node.DirectoryPath + @nodeUsage.Node.DirectoryPath - @project.DependsOnProjectsCount + @nodeUsage.DependsOnProjectsCount - @foreach (var childProject in project.DependsOnProjects) + @foreach (var childProject in nodeUsage.DependsOnProjects) { @childProject.Id } @@ -99,10 +108,10 @@ - @project.UsedByCount + @nodeUsage.UsedByCount - @foreach (var childProject in project.UsedBy) + @foreach (var childProject in nodeUsage.UsedBy) { @childProject.Id } @@ -113,10 +122,10 @@ - @project.DependsOnPackagesCount + @nodeUsage.DependsOnPackagesCount - @foreach (var package in project.DependsOnPackages) + @foreach (var package in nodeUsage.DependsOnPackages) { @package.Id [@package.Version] } @@ -125,8 +134,8 @@ - - + + @@ -144,7 +153,7 @@ private string? selectedSolution; - private List projects = []; + private List nodeUsageStatistics = []; private string? commonPrefix; @@ -184,10 +193,17 @@ } else if(SolutionRegistry.GetGraph(solution) is var graph && graph is not null) { - projects = graph + var projectUsageStatistics = graph .Nodes .OfType() - .Select(n => SolutionRegistry.GetDependencyCount(solution, n)).ToList(); + .Select(n => SolutionRegistry.GetDependencyCount(solution, n)); + + nodeUsageStatistics = new List + { + SolutionRegistry.GetDependencyCount(solution, solution) + }; + + nodeUsageStatistics.AddRange(projectUsageStatistics); displayMode = DisplayMode.Solutions; } From 5d2597ecfbad224d7d0d36874119c7f0d17c4f58 Mon Sep 17 00:00:00 2001 From: nikiforovall Date: Sun, 28 Jul 2024 17:51:07 +0300 Subject: [PATCH 7/7] docs: add readme --- README.md | 21 ++++++++++++++++++++- assets/serve-graph-view.png | 3 +++ assets/serve-main-window.png | 3 +++ assets/serve-terminal.png | 3 +++ 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 assets/serve-graph-view.png create mode 100644 assets/serve-main-window.png create mode 100644 assets/serve-terminal.png diff --git a/README.md b/README.md index 7da0ab2..d2edc0c 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,26 @@ COMMANDS: show Shows the dependencies of a project or solution located in the specified path ``` -## Example +## Usage + +You can start dependify in `serve mode` and open the browser to navigate the generated graph. + +```bash +dependify serve $dev/keycloak-authorization-services-dotnet/ +``` + +You will see the following output in the terminal. Open and browse the graph. + +![serve-terminal](./assets/serve-terminal.png) + +![serve-main-window](./assets/serve-main-window.png) + +You can open the mermaid diagram right in the browser. + +![serve-graph-view](./assets/serve-graph-view.png) + + +You can use the CLI for the automation or if you prefer the terminal. ```bash dependify graph scan \ diff --git a/assets/serve-graph-view.png b/assets/serve-graph-view.png new file mode 100644 index 0000000..1f03e7b --- /dev/null +++ b/assets/serve-graph-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b79c457d78aa547023479223b16d25fe06d4227fa232354505f6b6b460614d9a +size 147347 diff --git a/assets/serve-main-window.png b/assets/serve-main-window.png new file mode 100644 index 0000000..9df1990 --- /dev/null +++ b/assets/serve-main-window.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:141c227e7b6fda2fa463584a062956748b4bff6ac51bbed4e431d10a0f33611f +size 124304 diff --git a/assets/serve-terminal.png b/assets/serve-terminal.png new file mode 100644 index 0000000..ddfa330 --- /dev/null +++ b/assets/serve-terminal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ce1bcdd213083637aa3436eb2b22b668aa37e5906328110ba12da73ac1292e7 +size 25245