From 257f70612d8c64a31d0be935336add992d03fe82 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Thu, 18 Jun 2026 10:51:11 +0200 Subject: [PATCH 1/6] feat(ci): update CI workflow to validate automatic NuGet dependency addition and data-table model installation Enhanced the CI workflow to assert that the `shellui add` command correctly adds the necessary NuGet dependencies (Blazor-ApexCharts, System.Linq.Dynamic.Core) and verifies the installation of DataTable models. This ensures that the initialization process is robust and that all required components are present in the project file. --- .github/workflows/ci.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e9c1ac..bd34e98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,11 +65,17 @@ jobs: grep -q ':root' wwwroot/input.css || (echo "init did not write :root variables to input.css"; exit 1) grep -q '\.dark' wwwroot/input.css || (echo "init did not write .dark variables to input.css"; exit 1) - shellui add chart pie-chart dashboard-02 --force || true - # Chart components reference ApexCharts.* types but the CLI does not yet - # auto-add the runtime NuGet dep. Drop this line once `shellui add chart` - # declares Blazor-ApexCharts as a nuget dependency. - dotnet add package Blazor-ApexCharts --version 6.0.2 + shellui add chart pie-chart dashboard-02 data-table --force + + # NuGet dependencies (Blazor-ApexCharts, System.Linq.Dynamic.Core) should + # now be added automatically by `shellui add`. Assert they appear in the + # project file so a regression in the auto-install fails loudly here. + grep -q 'Blazor-ApexCharts' SmokeApp.csproj || (echo "shellui add chart did not add Blazor-ApexCharts NuGet dep"; exit 1) + grep -q 'System.Linq.Dynamic.Core' SmokeApp.csproj || (echo "shellui add data-table did not add System.Linq.Dynamic.Core NuGet dep"; exit 1) + + # And the DataTable models file must land at Components/UI/Models/, not be missing. + test -f Components/UI/Models/DataTableModels.cs || (echo "shellui add data-table did not install data-table-models"; exit 1) + dotnet build -c Debug - name: Upload build artifacts From ffbe34168662d97cab9e2e7c484e6c152e6f5b54 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Thu, 18 Jun 2026 10:51:29 +0200 Subject: [PATCH 2/6] feat(tests): add unit tests for NuGet dependencies and component suggestions Introduced new test classes for validating the functionality of the ComponentRegistry in suggesting component names and managing NuGet dependencies. The tests cover scenarios for finding closest matches for typos, ensuring hidden sub-components are not suggested, and verifying the registration and availability of NuGet dependencies for components like data-table and chart. This addition enhances test coverage and ensures the integrity of component suggestions and dependency management. --- ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs | 104 ++++++++++++++++++ src/ShellUI.Core/Models/NuGetDependency.cs | 7 ++ 2 files changed, 111 insertions(+) create mode 100644 ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs create mode 100644 src/ShellUI.Core/Models/NuGetDependency.cs diff --git a/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs b/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs new file mode 100644 index 0000000..e0833d3 --- /dev/null +++ b/ShellUI.Tests/NuGetDepsAndSuggestionsTests.cs @@ -0,0 +1,104 @@ +using System.Linq; +using ShellUI.Templates; +using Xunit; + +namespace ShellUI.Tests; + +public class RegistrySuggestionsTests +{ + [Theory] + [InlineData("datatable", "data-table")] + [InlineData("data_table", "data-table")] + [InlineData("buttong", "button")] + [InlineData("chrt", "chart")] + [InlineData("themetoggle", "theme-toggle")] + public void FindClosestMatch_SuggestsExpectedComponent(string typo, string expected) + { + Assert.Equal(expected, ComponentRegistry.FindClosestMatch(typo)); + } + + [Fact] + public void FindClosestMatch_ReturnsNullForExactMatch() + { + // Exact matches have distance 0 and are excluded from suggestions — + // the caller already knows the component exists. + Assert.Null(ComponentRegistry.FindClosestMatch("button")); + } + + [Fact] + public void FindClosestMatch_ReturnsNullWhenNothingIsClose() + { + Assert.Null(ComponentRegistry.FindClosestMatch("xyzzy-no-such-component-at-all")); + } + + [Fact] + public void FindClosestMatch_DoesNotSuggestHiddenSubComponents() + { + // `data-table-models` is IsAvailable=false (installed only as a dep of data-table). + // A user typo like "data-table-modls" should not be redirected to it. + var result = ComponentRegistry.FindClosestMatch("data-table-modls"); + Assert.NotEqual("data-table-models", result); + } +} + +public class NuGetDependenciesTests +{ + [Fact] + public void DataTableModels_IsRegisteredAndHidden() + { + var metadata = ComponentRegistry.GetMetadata("data-table-models"); + Assert.NotNull(metadata); + Assert.False(metadata!.IsAvailable, "data-table-models is a sub-component dep and must not appear in `shellui list`"); + } + + [Fact] + public void DataTable_DeclaresSystemLinqDynamicCore() + { + var metadata = ComponentRegistry.GetMetadata("data-table"); + Assert.NotNull(metadata); + Assert.Contains(metadata!.NuGetDependencies, p => p.PackageId == "System.Linq.Dynamic.Core"); + } + + [Fact] + public void Chart_DeclaresBlazorApexCharts() + { + var metadata = ComponentRegistry.GetMetadata("chart"); + Assert.NotNull(metadata); + Assert.Contains(metadata!.NuGetDependencies, p => p.PackageId == "Blazor-ApexCharts"); + } + + [Theory] + [InlineData("pie-chart")] + [InlineData("bar-chart")] + [InlineData("area-chart")] + [InlineData("line-chart")] + [InlineData("multi-series-chart")] + public void ChartVariants_TransitivelyPullInBlazorApexCharts(string componentName) + { + // Chart family components don't declare the NuGet dep themselves — they depend + // on `chart` which does. Verify the dependency chain is intact so the installer's + // recursive walk picks up the package. + var metadata = ComponentRegistry.GetMetadata(componentName); + Assert.NotNull(metadata); + Assert.Contains("chart", metadata!.Dependencies); + } +} + +public class DataTableTemplateContentTests +{ + // The library-wide convention is `Components.Models` for model namespaces regardless + // of where the file lives on disk (TabModels, StepperModels, ContextMenuModels, + // ChartModels all use this). The DataTable @using and DataTableModels namespace + // must agree on that convention so consumers can compile. + [Fact] + public void DataTable_UsingDirectiveMatchesDataTableModelsNamespace() + { + var dataTable = ComponentRegistry.GetComponentContent("data-table"); + var models = ComponentRegistry.GetComponentContent("data-table-models"); + + Assert.NotNull(dataTable); + Assert.NotNull(models); + Assert.Contains("@using YourProjectNamespace.Components.Models", dataTable); + Assert.Contains("namespace YourProjectNamespace.Components.Models;", models); + } +} diff --git a/src/ShellUI.Core/Models/NuGetDependency.cs b/src/ShellUI.Core/Models/NuGetDependency.cs new file mode 100644 index 0000000..d739f79 --- /dev/null +++ b/src/ShellUI.Core/Models/NuGetDependency.cs @@ -0,0 +1,7 @@ +namespace ShellUI.Core.Models; + +public class NuGetDependency +{ + public required string PackageId { get; init; } + public required string Version { get; init; } +} From 226acd618c152f0876833482c5ad73653f62f3be Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Thu, 18 Jun 2026 10:51:46 +0200 Subject: [PATCH 3/6] feat(templates): update DataTable models and template to include NuGet dependencies and mark models as unavailable Modified the DataTableModelsTemplate to set IsAvailable to false, indicating that the models are currently not available. Additionally, enhanced the DataTableTemplate to include a new NuGet dependency for System.Linq.Dynamic.Core, ensuring that the template has the necessary dependencies for advanced data table functionality. --- src/ShellUI.Templates/Templates/DataTableModelsTemplate.cs | 2 +- src/ShellUI.Templates/Templates/DataTableTemplate.cs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ShellUI.Templates/Templates/DataTableModelsTemplate.cs b/src/ShellUI.Templates/Templates/DataTableModelsTemplate.cs index 860e758..a5b8c38 100644 --- a/src/ShellUI.Templates/Templates/DataTableModelsTemplate.cs +++ b/src/ShellUI.Templates/Templates/DataTableModelsTemplate.cs @@ -11,7 +11,7 @@ public static class DataTableModelsTemplate Description = "Models for DataTable component", Category = ComponentCategory.DataDisplay, FilePath = "Models/DataTableModels.cs", - + IsAvailable = false, Dependencies = new List() }; diff --git a/src/ShellUI.Templates/Templates/DataTableTemplate.cs b/src/ShellUI.Templates/Templates/DataTableTemplate.cs index a7a12d3..62c168b 100644 --- a/src/ShellUI.Templates/Templates/DataTableTemplate.cs +++ b/src/ShellUI.Templates/Templates/DataTableTemplate.cs @@ -11,8 +11,11 @@ public static class DataTableTemplate Description = "Advanced data table with sorting, filtering, and pagination", Category = ComponentCategory.DataDisplay, FilePath = "DataTable.razor", - - Dependencies = new List { "data-table-models" } + Dependencies = new List { "data-table-models" }, + NuGetDependencies = new List + { + new() { PackageId = "System.Linq.Dynamic.Core", Version = "1.7.1" } + } }; public static string Content => @"@typeparam TItem From 2bea383f9178c47057d1915d01115098d42f67b0 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Thu, 18 Jun 2026 10:52:07 +0200 Subject: [PATCH 4/6] feat(templates): add NuGet dependency for Blazor-ApexCharts in ChartTemplate Enhanced the ChartTemplate to include a new NuGet dependency for Blazor-ApexCharts version 6.0.2, ensuring that the template has the necessary library for chart rendering functionality. --- src/ShellUI.Templates/Templates/ChartTemplate.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ShellUI.Templates/Templates/ChartTemplate.cs b/src/ShellUI.Templates/Templates/ChartTemplate.cs index 49c9751..478e9e8 100644 --- a/src/ShellUI.Templates/Templates/ChartTemplate.cs +++ b/src/ShellUI.Templates/Templates/ChartTemplate.cs @@ -12,6 +12,10 @@ public class ChartTemplate Category = ComponentCategory.DataDisplay, FilePath = "Chart.razor", Dependencies = new List { "chart-variants" }, + NuGetDependencies = new List + { + new() { PackageId = "Blazor-ApexCharts", Version = "6.0.2" } + }, Variants = new List { "default", "colorful", "monochrome" }, Tags = new List { "chart", "data", "visualization", "apexcharts" } }; From a4d443be44a328952b4f479969eeaa5d0c2c2078 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Thu, 18 Jun 2026 10:52:22 +0200 Subject: [PATCH 5/6] feat(templates): add data-table-models component and implement typo suggestion functionality Enhanced the ComponentRegistry by adding the "data-table-models" component and implementing a new method, FindClosestMatch, to suggest component names based on user input. This method utilizes Levenshtein distance to provide helpful hints for typos, improving user experience when searching for components. --- src/ShellUI.Templates/ComponentRegistry.cs | 51 ++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/ShellUI.Templates/ComponentRegistry.cs b/src/ShellUI.Templates/ComponentRegistry.cs index 35aa2eb..56238e0 100644 --- a/src/ShellUI.Templates/ComponentRegistry.cs +++ b/src/ShellUI.Templates/ComponentRegistry.cs @@ -133,6 +133,7 @@ public static class ComponentRegistry { "collapsible-trigger", CollapsibleTriggerTemplate.Metadata }, { "collapsible-content", CollapsibleContentTemplate.Metadata }, { "data-table", DataTableTemplate.Metadata }, + { "data-table-models", DataTableModelsTemplate.Metadata }, { "alert-dialog", AlertDialogTemplate.Metadata }, { "calendar", CalendarTemplate.Metadata }, { "loading", LoadingTemplate.Metadata }, @@ -303,6 +304,7 @@ public static class ComponentRegistry "collapsible-trigger" => CollapsibleTriggerTemplate.Content, "collapsible-content" => CollapsibleContentTemplate.Content, "data-table" => DataTableTemplate.Content, + "data-table-models" => DataTableModelsTemplate.Content, "alert-dialog" => AlertDialogTemplate.Content, "calendar" => CalendarTemplate.Content, "loading" => LoadingTemplate.Content, @@ -364,5 +366,54 @@ public static bool Exists(string componentName) { return Components.ContainsKey(componentName.ToLower()); } + + // Returns the closest installable component name to `query` within edit distance 3, + // or null if nothing is close enough. Used to power "did you mean …?" hints when a + // user mistypes (e.g. `datatable` → `data-table`). Excludes hidden sub-components. + public static string? FindClosestMatch(string query) + { + var lower = query.ToLowerInvariant(); + string? best = null; + var bestDistance = int.MaxValue; + + foreach (var (name, metadata) in Components) + { + if (!metadata.IsAvailable) continue; + var d = LevenshteinDistance(lower, name); + if (d == 0 || d > 3) continue; + if (d < bestDistance || (d == bestDistance && string.CompareOrdinal(name, best) < 0)) + { + best = name; + bestDistance = d; + } + } + + return best; + } + + private static int LevenshteinDistance(string a, string b) + { + if (a.Length == 0) return b.Length; + if (b.Length == 0) return a.Length; + + var prev = new int[b.Length + 1]; + var curr = new int[b.Length + 1]; + for (var j = 0; j <= b.Length; j++) prev[j] = j; + + for (var i = 1; i <= a.Length; i++) + { + curr[0] = i; + for (var j = 1; j <= b.Length; j++) + { + var cost = a[i - 1] == b[j - 1] ? 0 : 1; + curr[j] = Math.Min( + Math.Min(curr[j - 1] + 1, prev[j] + 1), + prev[j - 1] + cost); + } + (prev, curr) = (curr, prev); + } + + return prev[b.Length]; + } } From bb2485e62fd832247952dcfa595816991ea36543 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Thu, 18 Jun 2026 10:52:41 +0200 Subject: [PATCH 6/6] feat(components): enhance ComponentInstaller to manage NuGet dependencies and improve installation process Updated the ComponentInstaller to track and install NuGet dependencies more efficiently. Introduced a new method, InstallNuGetDependenciesAsync, to handle the addition of packages after all components are processed, preventing multiple invocations of `dotnet add package`. Enhanced the InstallComponentWithDependencies method to include suggestions for typos in component names, improving user experience. Additionally, added a NuGetDependencies property to ComponentMetadata for better dependency management. --- .../Services/ComponentInstaller.cs | 92 +++++++++++++++++-- src/ShellUI.Core/Models/ComponentMetadata.cs | 6 ++ 2 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/ShellUI.CLI/Services/ComponentInstaller.cs b/src/ShellUI.CLI/Services/ComponentInstaller.cs index aee3f14..3a799a9 100644 --- a/src/ShellUI.CLI/Services/ComponentInstaller.cs +++ b/src/ShellUI.CLI/Services/ComponentInstaller.cs @@ -10,7 +10,7 @@ public class ComponentInstaller public static async Task InstallComponents(string[] components, bool force) { var configPath = Path.Combine(Directory.GetCurrentDirectory(), "shellui.json"); - + if (!File.Exists(configPath)) { AnsiConsole.MarkupLine("[red]ShellUI not initialized![/]"); @@ -21,7 +21,7 @@ public static async Task InstallComponents(string[] components, bool force) // Load config var configJson = File.ReadAllText(configPath); var config = JsonSerializer.Deserialize(configJson); - + if (config == null) { AnsiConsole.MarkupLine("[red]Failed to read shellui.json[/]"); @@ -44,7 +44,10 @@ public static async Task InstallComponents(string[] components, bool force) // Track installed components to avoid duplicates var installedSet = new HashSet(); - + // Track NuGet packages requested this batch so we don't re-invoke `dotnet add package` for the same dep + var requestedPackages = new HashSet(StringComparer.OrdinalIgnoreCase); + var pendingNuGetDeps = new List(); + // Show dependency information foreach (var componentName in componentList) { @@ -54,9 +57,9 @@ public static async Task InstallComponents(string[] components, bool force) AnsiConsole.MarkupLine($"[green]●[/] [bold]{componentName}[/] requires: [yellow]{string.Join(", ", metadata.Dependencies)}[/]"); } } - + AnsiConsole.MarkupLine(""); - + await AnsiConsole.Status() .Spinner(Spinner.Known.Dots) .SpinnerStyle(Style.Parse("green")) @@ -65,11 +68,15 @@ await AnsiConsole.Status() foreach (var componentName in componentList) { ctx.Status($"Installing {componentName}..."); - InstallComponentWithDependencies(componentName, config, projectInfo, force, installedSet, ref successCount, ref skippedCount, failedComponents); + InstallComponentWithDependencies(componentName, config, projectInfo, force, installedSet, requestedPackages, pendingNuGetDeps, ref successCount, ref skippedCount, failedComponents); } return Task.CompletedTask; }); + // Install collected NuGet dependencies once, after all source files are in place + // (so `dotnet add package` doesn't restore between every component). + await InstallNuGetDependenciesAsync(projectInfo, pendingNuGetDeps); + // Update config var updatedJson = JsonSerializer.Serialize(config, new JsonSerializerOptions { @@ -203,14 +210,19 @@ private static InstallResult InstallComponentInternal(string componentName, Shel return InstallResult.Success; } - private static void InstallComponentWithDependencies(string componentName, ShellUIConfig config, ProjectInfo projectInfo, bool force, HashSet installedSet, ref int successCount, ref int skippedCount, List failedComponents) + private static void InstallComponentWithDependencies(string componentName, ShellUIConfig config, ProjectInfo projectInfo, bool force, HashSet installedSet, HashSet requestedPackages, List pendingNuGetDeps, ref int successCount, ref int skippedCount, List failedComponents) { if (installedSet.Contains(componentName)) return; // Already processed - + if (!ComponentRegistry.Exists(componentName)) { AnsiConsole.MarkupLine($"[red]Component '{componentName}' not found[/]"); + var suggestion = ComponentRegistry.FindClosestMatch(componentName); + if (suggestion != null) + { + AnsiConsole.MarkupLine($"[yellow]Did you mean '[bold]{suggestion}[/]'?[/]"); + } failedComponents.Add(componentName); return; } @@ -231,14 +243,14 @@ private static void InstallComponentWithDependencies(string componentName, Shell { if (!installedSet.Contains(dep)) { - InstallComponentWithDependencies(dep, config, projectInfo, force, installedSet, ref successCount, ref skippedCount, failedComponents); + InstallComponentWithDependencies(dep, config, projectInfo, force, installedSet, requestedPackages, pendingNuGetDeps, ref successCount, ref skippedCount, failedComponents); } } } // Install the component itself var result = InstallComponentInternal(componentName, config, projectInfo, force); - + if (result == InstallResult.Success) { successCount++; @@ -253,6 +265,66 @@ private static void InstallComponentWithDependencies(string componentName, Shell { failedComponents.Add(componentName); } + + // Collect NuGet deps regardless of source-file install result — even a `Skipped` + // file still requires its NuGet packages to compile. + if (result != InstallResult.Failed && metadata.NuGetDependencies?.Any() == true) + { + foreach (var pkg in metadata.NuGetDependencies) + { + if (requestedPackages.Add(pkg.PackageId)) + { + pendingNuGetDeps.Add(pkg); + } + } + } + } + + private static async Task InstallNuGetDependenciesAsync(ProjectInfo projectInfo, List deps) + { + if (deps.Count == 0) return; + + AnsiConsole.MarkupLine(""); + AnsiConsole.MarkupLine($"[cyan]Adding {deps.Count} NuGet package reference(s)...[/]"); + + foreach (var dep in deps) + { + var startInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = "dotnet", + ArgumentList = { "add", projectInfo.ProjectPath, "package", dep.PackageId, "--version", dep.Version }, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + try + { + using var process = System.Diagnostics.Process.Start(startInfo); + if (process == null) + { + AnsiConsole.MarkupLine($"[yellow]Warning:[/] could not start `dotnet add package` for {dep.PackageId}"); + continue; + } + + await process.WaitForExitAsync(); + + if (process.ExitCode == 0) + { + AnsiConsole.MarkupLine($"[green]Added:[/] {dep.PackageId} {dep.Version}"); + } + else + { + var err = (await process.StandardError.ReadToEndAsync()).Trim(); + AnsiConsole.MarkupLine($"[yellow]Warning:[/] failed to add {dep.PackageId}: {err.Replace("[", "[[").Replace("]", "]]")}"); + } + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[yellow]Warning:[/] could not add {dep.PackageId}: {ex.Message.Replace("[", "[[").Replace("]", "]]")}"); + } + } } private enum InstallResult diff --git a/src/ShellUI.Core/Models/ComponentMetadata.cs b/src/ShellUI.Core/Models/ComponentMetadata.cs index 52e3738..4e93c77 100644 --- a/src/ShellUI.Core/Models/ComponentMetadata.cs +++ b/src/ShellUI.Core/Models/ComponentMetadata.cs @@ -12,6 +12,12 @@ public class ComponentMetadata public required ComponentCategory Category { get; set; } public List Dependencies { get; set; } = new(); + // NuGet packages the rendered component references at compile time (e.g. Chart + // uses Blazor-ApexCharts; DataTable uses System.Linq.Dynamic.Core). The CLI + // runs `dotnet add package` for each on install — without this the consumer + // sees CS0246 errors after `shellui add`. + public List NuGetDependencies { get; set; } = new(); + // Relative to Components/UI folder (or LayoutPath when IsLayoutBlock is true) public required string FilePath { get; set; }