diff --git a/README.md b/README.md index 64f11b2..4afac30 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,22 @@ Usage: mermaid-graph [options] Options: - --path Full path to the solution (*.sln) or project (*.csproj) file that will be mapped. - --version Show version information - -?, -h, --help Show help and usage information - ``` + --path Full path to the solution (*.sln) or project (*.csproj) file that will be mapped. + --type The type of diagram to generate (e.g., Graph or Class). [default: Graph] + --version Show version information + -?, -h, --help Show help and usage information +``` ## Example output from this solution +You can run the following command to generate a class diagram for this solution: + +```powershell +.\mermaid-graph.exe --path "MermaidGraph.NET.sln" --type Class +``` + +This will generate a mermaid graph in the console output, which can be piped to a file and used in a markdown document. + ```mermaid --- title: MermaidGraph.NET.sln @@ -78,6 +87,16 @@ classDiagram version 6.0.4 } MermaidGraphTests ..> coverlet.msbuild + class Microsoft.ClearScript.V8{ + type NuGet + version 7.4.5 + } + MermaidGraphTests ..> Microsoft.ClearScript.V8 + class Microsoft.ClearScript.V8.Native.win-x64{ + type NuGet + version 7.4.5 + } + MermaidGraphTests ..> Microsoft.ClearScript.V8.Native.win-x64 class Microsoft.NET.Test.Sdk{ type NuGet version 17.13.0 diff --git a/mermaid-graph/Commands.cs b/mermaid-graph/Commands.cs index 890246d..32e3547 100644 --- a/mermaid-graph/Commands.cs +++ b/mermaid-graph/Commands.cs @@ -1,143 +1,40 @@ -using System.Text; -using Microsoft.Build.Construction; -using Microsoft.Build.Evaluation; -using Microsoft.Build.Locator; +using MermaidGraph.Diagrams; namespace MermaidGraph; /// -/// The commands that can be run by `mermaid-graph` +/// The commands that can be run by `mermaid-graph`. /// public class Commands { - public const string MermaidBegin = Fence + "mermaid"; - public const string Fence = "```"; - - private readonly StringBuilder _graph; - - /// - /// Initialize the graph output - /// - public Commands() - { - _graph = new StringBuilder(); - - // Ensure MSBuild is registered - if (!MSBuildLocator.IsRegistered) - { - MSBuildLocator.RegisterDefaults(); - } - } - /// /// Generate the dependency graph of a Visual Studio Project. /// /// `.csproj` file. - public string Project(FileInfo file) + /// + public static string Project(FileInfo file, DiagramType diagramType = DiagramType.Graph) { - Header(file.Name); - using var projectCollection = new ProjectCollection(); - var project = projectCollection.LoadProject(file.FullName); - GraphProject(project); - _graph.AppendLine(Fence); - var graph = _graph.ToString(); - - // Cleanup - _graph.Clear(); - projectCollection.UnloadAllProjects(); - - return graph; + var graph = GetGraphType(diagramType); + + return graph.Project(file); } /// /// Generate the dependency graph of a Visual Studio Solution. /// /// `.sln` file. - public string Solution(FileInfo file) + /// + public static string Solution(FileInfo file, DiagramType diagramType = DiagramType.Graph) { - Header(file.Name); - var solutionFile = SolutionFile.Parse(file.FullName); - var solutionName = Path.GetFileNameWithoutExtension(file.Name); - var solutionId = $"{solutionName}"; - _graph.AppendLine($$""" - class {{solutionName}}{ - type solution - } - """); - - using var projectCollection = new ProjectCollection(); + var graph = GetGraphType(diagramType); - foreach (var project in solutionFile.ProjectsInOrder) - { - if (project.ProjectType != SolutionProjectType.KnownToBeMSBuildFormat) continue; - - var projectPath = project.AbsolutePath; - var projectName = Path.GetFileNameWithoutExtension(projectPath); - _graph.AppendLine($" {solutionId} --> {projectName}"); - var projectFile = new FileInfo(projectPath); - if (projectFile.Exists) - { - var referenceProject = projectCollection.LoadProject(projectFile.FullName); - GraphProject(referenceProject); - } - } - - _graph.AppendLine(Fence); - var graph = _graph.ToString(); - - // Cleanup - _graph.Clear(); - projectCollection.UnloadAllProjects(); - - return graph; - } - - private void Header(string title) - { - _graph.AppendLine(MermaidBegin); - _graph.AppendLine($""" - --- - title: {title} - config: - class: - hideEmptyMembersBox: true - --- - """); - - _graph.AppendLine("classDiagram"); + return graph.Solution(file); } - private void GraphProject(Project project) + private static IMermaidDiagram GetGraphType(DiagramType diagramType) => diagramType switch { - var projectName = Path.GetFileNameWithoutExtension(project.FullPath); - var type = project.GetPropertyValue("OutputType"); - var targetFramework = project.GetPropertyValue("TargetFramework") ?? project.GetPropertyValue("TargetFrameworks"); - _graph.AppendLine($$""" - class {{projectName}}{ - type {{type}} - target {{targetFramework}} - } - """); - - foreach (var item in project.GetItems("ProjectReference")) - { - var refPath = item.EvaluatedInclude; - var refName = Path.GetFileNameWithoutExtension(refPath); - _graph.AppendLine($" {projectName} ..> {refName}"); - } - - foreach (var item in project.GetItems("PackageReference")) - { - var packageName = item.EvaluatedInclude; - var version = item.GetMetadataValue("Version"); - _graph.AppendLine($$""" - class {{packageName}}{ - type NuGet - version {{version}} - } - """); - - _graph.AppendLine($" {projectName} ..> {packageName}"); - } - } + DiagramType.Class => new ClassDiagram(), + DiagramType.Graph => new GraphDiagram(), + _ => throw new NotImplementedException($"Option not supported: {diagramType}"), + }; } \ No newline at end of file diff --git a/mermaid-graph/Diagrams/ClassDiagram.cs b/mermaid-graph/Diagrams/ClassDiagram.cs new file mode 100644 index 0000000..f69e09f --- /dev/null +++ b/mermaid-graph/Diagrams/ClassDiagram.cs @@ -0,0 +1,105 @@ +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; + +namespace MermaidGraph.Diagrams; + +internal class ClassDiagram : MermaidDiagram +{ + /// + public override void Header(string title) + { + base.Header(title); + Graph.AppendLine("classDiagram"); + } + + /// + /// Generate the dependency graph of a Visual Studio Project. + /// + /// `.csproj` file. + public override string Project(FileInfo file) + { + Header(file.Name); + using var projectCollection = new ProjectCollection(); + var project = projectCollection.LoadProject(file.FullName); + GraphProject(project); + Graph.AppendLine(Fence); + + projectCollection.UnloadAllProjects(); + + return Graph.ToString(); + } + + /// + /// Generate the dependency graph of a Visual Studio Solution. + /// + /// `.sln` file. + public override string Solution(FileInfo file) + { + Header(file.Name); + var solutionFile = SolutionFile.Parse(file.FullName); + var solutionName = Path.GetFileNameWithoutExtension(file.Name); + var solutionId = $"{solutionName}"; + Graph.AppendLine($$""" + class {{solutionName}}{ + type solution + } + """); + + using var projectCollection = new ProjectCollection(); + + foreach (var project in solutionFile.ProjectsInOrder) + { + if (project.ProjectType != SolutionProjectType.KnownToBeMSBuildFormat) continue; + + var projectPath = project.AbsolutePath; + var projectName = Path.GetFileNameWithoutExtension(projectPath); + Graph.AppendLine($" {solutionId} --> {projectName}"); + var projectFile = new FileInfo(projectPath); + if (projectFile.Exists) + { + var referenceProject = projectCollection.LoadProject(projectFile.FullName); + GraphProject(referenceProject); + } + } + + Graph.AppendLine(Fence); + + projectCollection.UnloadAllProjects(); + + return Graph.ToString(); + } + + private void GraphProject(Project project) + { + var projectName = Path.GetFileNameWithoutExtension(project.FullPath); + var type = project.GetPropertyValue("OutputType"); + var targetFramework = project.GetPropertyValue("TargetFramework") ?? project.GetPropertyValue("TargetFrameworks"); + Graph.AppendLine($$""" + class {{projectName}}{ + type {{type}} + target {{targetFramework}} + } + """); + + foreach (var item in project.GetItems("ProjectReference")) + { + var refPath = item.EvaluatedInclude; + var refName = Path.GetFileNameWithoutExtension(refPath); + Graph.AppendLine($" {projectName} ..> {refName}"); + } + + foreach (var item in project.GetItems("PackageReference")) + { + var packageName = item.EvaluatedInclude; + var version = item.GetMetadataValue("Version"); + Graph.AppendLine($$""" + class {{packageName}}{ + type NuGet + version {{version}} + } + """); + + Graph.AppendLine($" {projectName} ..> {packageName}"); + } + } +} \ No newline at end of file diff --git a/mermaid-graph/Diagrams/DiagramType.cs b/mermaid-graph/Diagrams/DiagramType.cs new file mode 100644 index 0000000..d580ec6 --- /dev/null +++ b/mermaid-graph/Diagrams/DiagramType.cs @@ -0,0 +1,17 @@ +namespace MermaidGraph.Diagrams; + +/// +/// Specifies the type of mermaid diagram to generate. +/// +public enum DiagramType +{ + /// + /// Represents a general dependency graph. + /// + Graph, + + /// + /// Represents a class diagram. + /// + Class +} \ No newline at end of file diff --git a/mermaid-graph/Diagrams/GraphDiagram.cs b/mermaid-graph/Diagrams/GraphDiagram.cs new file mode 100644 index 0000000..809a36a --- /dev/null +++ b/mermaid-graph/Diagrams/GraphDiagram.cs @@ -0,0 +1,84 @@ +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; + +namespace MermaidGraph.Diagrams; + +internal class GraphDiagram : MermaidDiagram +{ + /// + public override void Header(string title) + { + base.Header(title); + Graph.AppendLine("graph TD"); + } + + /// + /// Generate the dependency graph of a Visual Studio Project. + /// + /// `.csproj` file. + public override string Project(FileInfo file) + { + Header(file.Name); + using var projectCollection = new ProjectCollection(); + var project = projectCollection.LoadProject(file.FullName); + GraphProject(project); + Graph.AppendLine(Fence); + + projectCollection.UnloadAllProjects(); + + return Graph.ToString(); + } + + /// + /// Generate the dependency graph of a Visual Studio Solution. + /// + /// `.sln` file. + public override string Solution(FileInfo file) + { + Header(file.Name); + var solutionFile = SolutionFile.Parse(file.FullName); + var solutionName = Path.GetFileNameWithoutExtension(file.Name); + var solutionId = $"s{solutionFile.GetHashCode()}({solutionName})"; + + using var projectCollection = new ProjectCollection(); + + foreach (var project in solutionFile.ProjectsInOrder) + { + if (project.ProjectType != SolutionProjectType.KnownToBeMSBuildFormat) continue; + + var projectPath = project.AbsolutePath; + var projectName = Path.GetFileNameWithoutExtension(projectPath); + Graph.AppendLine($" {solutionId} --> {projectName}"); + var projectFile = new FileInfo(projectPath); + if (projectFile.Exists) + { + var referenceProject = projectCollection.LoadProject(projectFile.FullName); + GraphProject(referenceProject); + } + } + + Graph.AppendLine(Fence); + + projectCollection.UnloadAllProjects(); + + return Graph.ToString(); + } + + private void GraphProject(Project project) + { + var projectName = Path.GetFileNameWithoutExtension(project.FullPath); + + foreach (var item in project.GetItems("ProjectReference")) + { + var refPath = item.EvaluatedInclude; + var refName = Path.GetFileNameWithoutExtension(refPath); + Graph.AppendLine($" {projectName} --> {refName}"); + } + + foreach (var item in project.GetItems("PackageReference")) + { + var packageName = item.EvaluatedInclude; + Graph.AppendLine($" {projectName} -->|NuGet| {packageName}"); + } + } +} \ No newline at end of file diff --git a/mermaid-graph/Diagrams/IMermaidDiagram.cs b/mermaid-graph/Diagrams/IMermaidDiagram.cs new file mode 100644 index 0000000..d9c6221 --- /dev/null +++ b/mermaid-graph/Diagrams/IMermaidDiagram.cs @@ -0,0 +1,23 @@ +namespace MermaidGraph.Diagrams; + +/// +/// This file defines the IMermaidDiagram interface and the MermaidDiagram abstract class. +/// The IMermaidDiagram interface provides methods for generating Mermaid diagrams +/// from Visual Studio project (*.csproj) and solution (*.sln) files. +/// +public interface IMermaidDiagram +{ + /// + /// Generate the diagram from a visual studio project file (*.csproj) + /// + /// The project file + /// Mermaid Markdown + public string Project(FileInfo file); + + /// + /// Generate the diagram from a visual studio solution file (*.sln) + /// + /// The solution file. + /// Mermaid Markdown + public string Solution(FileInfo file); +} \ No newline at end of file diff --git a/mermaid-graph/Diagrams/MermaidDiagram.cs b/mermaid-graph/Diagrams/MermaidDiagram.cs new file mode 100644 index 0000000..8aeebdc --- /dev/null +++ b/mermaid-graph/Diagrams/MermaidDiagram.cs @@ -0,0 +1,63 @@ +using System.Text; +using Microsoft.Build.Locator; + +namespace MermaidGraph.Diagrams; + +/// +/// The MermaidDiagram abstract class implements shared functionality for Mermaid diagram generation, +/// including initializing the graph output and managing the graph buffer. +/// +public abstract class MermaidDiagram : IMermaidDiagram +{ + /// + /// Code block fence. + /// + public const string Fence = "```"; + + /// + /// Mermaid code block. + /// + public const string MermaidBegin = Fence + "mermaid"; + + internal readonly StringBuilder Graph = new(); + + /// + /// Initialize the MermaidDiagram class and ensure MSBuild is registered. + /// + protected MermaidDiagram() + { + if (!MSBuildLocator.IsRegistered) + { + MSBuildLocator.RegisterDefaults(); + } + } + + /// + /// Initialize the graph output. + /// + public virtual void Header(string title) + { + Graph.Clear(); + Graph.AppendLine(MermaidBegin); + Graph.AppendLine($""" + --- + title: {title} + config: + class: + hideEmptyMembersBox: true + --- + """); + } + + /// + /// Get the mermaid diagram Markdown text. + /// + /// The contents of the graph buffer. + public override string ToString() => Graph.ToString(); + + /// + public abstract string Project(FileInfo file); + + /// + public abstract string Solution(FileInfo file); +} diff --git a/mermaid-graph/Program.cs b/mermaid-graph/Program.cs index 4726c6a..4a52f4d 100644 --- a/mermaid-graph/Program.cs +++ b/mermaid-graph/Program.cs @@ -1,6 +1,6 @@ -namespace MermaidGraph; +using MermaidGraph.Diagrams; -// ReSharper disable UnusedMember.Global +namespace MermaidGraph; /// /// mermaid-graph.exe @@ -11,15 +11,18 @@ public sealed class Program /// Outputs a mermaid graph of the dependency diagram for a project, or whole solution. /// /// Full path to the solution (*.sln) or project (*.csproj) file that will be mapped. + /// The type of diagram to generate (e.g., Graph or Class). /// HResult - public static int Main(string? path) + public static int Main(string? path, DiagramType type = DiagramType.Graph) { if (path is null) { - System.CommandLine.DragonFruit.CommandLine.ExecuteAssembly(typeof(AutoGeneratedProgram).Assembly, ["--help"], ""); + System.CommandLine.DragonFruit.CommandLine + .ExecuteAssembly(typeof(AutoGeneratedProgram).Assembly, ["--help"], ""); + return 1; } - + var file = new FileInfo(path); if (!file.Exists) { @@ -31,13 +34,13 @@ public static int Main(string? path) { if (path.EndsWith(".csproj")) { - Console.WriteLine(new Commands().Project(file)); + Console.WriteLine(Commands.Project(file, type)); return 0; } if (path.EndsWith(".sln")) { - Console.WriteLine(new Commands().Solution(file)); + Console.WriteLine(Commands.Solution(file, type)); return 0; } } @@ -50,4 +53,4 @@ public static int Main(string? path) Console.WriteLine($"Error: Unsupported file type - {path}"); return 3; } -} \ No newline at end of file +} diff --git a/mermaid-graph/mermaid-graph.csproj b/mermaid-graph/mermaid-graph.csproj index 2e7fa9a..aead51d 100644 --- a/mermaid-graph/mermaid-graph.csproj +++ b/mermaid-graph/mermaid-graph.csproj @@ -26,7 +26,7 @@ True mermaid-graph.png mermaid-graph.ico - 1.0.0-beta + 1.0.0 LICENSE true diff --git a/mermaid-graphTests/CommandsTests.cs b/mermaid-graphTests/CommandsTests.cs index 7adb517..1ec7da0 100644 --- a/mermaid-graphTests/CommandsTests.cs +++ b/mermaid-graphTests/CommandsTests.cs @@ -1,4 +1,5 @@ -using Microsoft.ClearScript.V8; +using MermaidGraph.Diagrams; +using Microsoft.ClearScript.V8; using NUnit.Framework; using Assert = NUnit.Framework.Assert; @@ -22,6 +23,12 @@ private V8ScriptEngine Js { } } + internal static readonly object[] DiagramTypeTestCases = + [ + new object[] { DiagramType.Class, "class" }, + new object[] { DiagramType.Graph, "flowchart" } + ]; + [OneTimeTearDown] public void Disposal() { @@ -29,34 +36,36 @@ public void Disposal() } [Test] - public void DogFoodSolutionTest() + [TestCaseSource(nameof(DiagramTypeTestCases))] + public void DogFoodSolutionTest(DiagramType type, string typeName) { var solutionPath = FindFileDownTree("*.sln"); Assert.That(solutionPath, Is.Not.Null); var info = new FileInfo(solutionPath!); Assert.That(info.Exists); - var graph = new Commands().Solution(info); + var graph = Commands.Solution(info, type); Console.WriteLine(graph); var graphType = DetectType(ExtractMermaid(graph)); - Assert.That(graphType, Is.EqualTo("class")); + Assert.That(graphType, Is.EqualTo(typeName)); Console.WriteLine(graphType); } [Test] - public void DogFoodProjectTestAsync() + [TestCaseSource(nameof(DiagramTypeTestCases))] + public void DogFoodProjectTest(DiagramType type, string typeName) { var filePath = FindFileDownTree("*.csproj"); Assert.That(filePath, Is.Not.Null); var info = new FileInfo(filePath!); Assert.That(info.Exists); - var graph = new Commands().Project(info); + var graph = Commands.Project(info, type); Console.WriteLine(graph); var graphType = DetectType(ExtractMermaid(graph)); - Assert.That(graphType, Is.EqualTo("class")); + Assert.That(graphType, Is.EqualTo(typeName)); Console.WriteLine(graphType); } @@ -88,13 +97,13 @@ public void CommandLineFailTests(string? file, int hResult) Assert.That(Program.Main(file), Is.EqualTo(hResult)); } - private static string ExtractMermaid(string markup) + private static string ExtractMermaid(string? markup) { - Assert.That(markup, Does.StartWith(Commands.MermaidBegin)); - markup = markup.Substring(Commands.MermaidBegin.Length + Environment.NewLine.Length); + Assert.That(markup, Does.StartWith(MermaidDiagram.MermaidBegin)); + markup = markup.Substring(MermaidDiagram.MermaidBegin.Length + Environment.NewLine.Length); - Assert.That(markup, Does.EndWith(Commands.Fence + Environment.NewLine)); - return markup.Substring(0, markup.Length - Commands.MermaidBegin.Length + Environment.NewLine.Length); + Assert.That(markup, Does.EndWith(MermaidDiagram.Fence + Environment.NewLine)); + return markup.Substring(0, markup.Length - MermaidDiagram.MermaidBegin.Length + Environment.NewLine.Length); } private string? DetectType(string markup)