From a350cf8f6991b9ae33db40b968d8895862204f70 Mon Sep 17 00:00:00 2001 From: Shewatipa Tseisi Date: Wed, 30 Sep 2026 09:53:39 +0200 Subject: [PATCH 1/2] fix: port v0.3.0 fixes to main - input, alert, badge and toggle no longer import a Variants namespace they don't install. - chart-series depends on chart, which brings the Blazor-ApexCharts package. - .NET 8/9 projects named like my-app get a valid namespace (my_app). - Test: every direct target installs the namespaces and packages it imports. - Test: namespace sanitizing. - CI: the all-components check builds .NET 10, 9 and 8 apps with hyphenated names. --- .github/workflows/ci.yml | 15 +++--- ShellUI.Tests/InitBootstrapTests.cs | 15 ++++++ ShellUI.Tests/TemplateCompileTests.cs | 53 +++++++++++++++++++ src/ShellUI.CLI/Services/ProjectDetector.cs | 10 +++- .../Templates/AlertTemplate.cs | 1 - .../Templates/BadgeTemplate.cs | 1 - .../Templates/ChartSeriesTemplate.cs | 2 +- .../Templates/InputTemplate.cs | 1 - .../Templates/ToggleTemplate.cs | 1 - 9 files changed, 87 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9857ab..22f45cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -158,12 +158,15 @@ jobs: print(" ".join(names)) PY ) - cd "$TMPDIR/app" - dotnet new blazor -o AllApp --no-restore - cd AllApp - shellui init --tailwind standalone --yes - shellui add $ALL - dotnet build -c Debug + # The CLI's source must build in .NET 8 and 9 projects too, and hyphenated names test the namespace sanitizing. + for fw in net10.0 net9.0 net8.0; do + cd "$TMPDIR/app" + dotnet new blazor -o "AllApp-$fw" -f "$fw" --no-restore + cd "AllApp-$fw" + shellui init --tailwind standalone --yes + shellui add $ALL + dotnet build -c Debug + done # Pure-NuGet install path — `dotnet add package ShellUI.Components` without # the CLI. Uses a one-off NuGet.config that whitelists ONLY the local feed, diff --git a/ShellUI.Tests/InitBootstrapTests.cs b/ShellUI.Tests/InitBootstrapTests.cs index 4394e8e..5618cfd 100644 --- a/ShellUI.Tests/InitBootstrapTests.cs +++ b/ShellUI.Tests/InitBootstrapTests.cs @@ -191,3 +191,18 @@ public void BaseLayer_HidesFocusOutlineOnNavigatedHeading() Assert.Contains("h1:focus {\n outline: none;", ShellUI.Templates.CssTemplates.InputCss.Replace("\r\n", "\n")); } } + +public class RootNamespaceTests +{ + // Matches what the compiler and the .NET 10 template produce for the same project names. + [Theory] + [InlineData("my-app", "my_app")] + [InlineData("AllApp-net9.0", "AllApp_net9._0")] + [InlineData("Contoso.Shop", "Contoso.Shop")] + [InlineData("1app", "_1app")] + [InlineData("My App", "My_App")] + public void SanitizeNamespace_MakesAValidNamespace(string value, string expected) + { + Assert.Equal(expected, ProjectDetector.SanitizeNamespace(value)); + } +} diff --git a/ShellUI.Tests/TemplateCompileTests.cs b/ShellUI.Tests/TemplateCompileTests.cs index 227a011..3295137 100644 --- a/ShellUI.Tests/TemplateCompileTests.cs +++ b/ShellUI.Tests/TemplateCompileTests.cs @@ -2,6 +2,7 @@ using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using ShellUI.Core.Models; using ShellUI.Templates; using Xunit; @@ -142,6 +143,58 @@ public void EveryHiddenEntry_IsReachableFromAnInstallableTarget() Assert.True(orphans.Count == 0, "Hidden entries no installable target depends on:\n " + string.Join("\n ", orphans)); } + // `shellui add ` alone must compile: every project namespace a file imports has to be declared + // by a file the same install writes. The all-components CI sweep can't see this, since other files fill the gap. + [Fact] + public void EveryDirectTarget_ImportsOnlyNamespacesItInstalls() + { + var declaration = new Regex(@"^\s*@?namespace\s+YourProjectNamespace(\.[\w.]+)?", RegexOptions.Multiline); + var import = new Regex(@"^\s*@?using\s+YourProjectNamespace(\.[\w.]+)?\s*;?\s*$", RegexOptions.Multiline); + // Present in every project: init installs shell and shellui-js, and files without @namespace use folder namespaces. + var always = new[] { "", ".Components", ".Components.UI", ".Components.Layout" }; + + var failures = new List(); + foreach (var (name, metadata) in ComponentRegistry.Components.Where(c => c.Value.IsAvailable)) + { + var closure = new HashSet(); + var stack = new Stack(new[] { name, "shell", "shellui-js" }); + while (stack.Count > 0) + { + var current = stack.Pop(); + if (!closure.Add(current)) continue; + foreach (var dep in ComponentRegistry.Components[current].Dependencies ?? new List()) + stack.Push(dep); + } + + var contents = closure.Select(n => ComponentRegistry.GetComponentContent(n) ?? "").ToList(); + var declared = contents.SelectMany(c => declaration.Matches(c).Select(m => m.Groups[1].Value)).Concat(always).ToHashSet(); + var missing = contents.SelectMany(c => import.Matches(c).Select(m => m.Groups[1].Value)) + .Where(ns => !declared.Contains(ns)) + .Distinct() + .ToList(); + if (missing.Count > 0) + failures.Add($"{name}: {string.Join(", ", missing.Select(ns => "YourProjectNamespace" + ns))}"); + + var packages = closure.SelectMany(n => ComponentRegistry.Components[n].NuGetDependencies ?? new List()) + .Select(p => p.PackageId) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (var (ns, package) in ThirdPartyNamespaces) + { + var usesIt = new Regex($@"^\s*@?using\s+{Regex.Escape(ns)}\s*;?\s*$", RegexOptions.Multiline); + if (contents.Any(c => usesIt.IsMatch(c)) && !packages.Contains(package)) + failures.Add($"{name}: imports {ns} without the {package} package"); + } + } + + Assert.True(failures.Count == 0, "Targets whose imports are not installed with them:\n " + string.Join("\n ", failures)); + } + + private static readonly (string Namespace, string Package)[] ThirdPartyNamespaces = + { + ("ApexCharts", "Blazor-ApexCharts"), + ("System.Linq.Dynamic.Core", "System.Linq.Dynamic.Core") + }; + /// Strips Razor markup directives so the remaining text can be best-effort /// parsed as C#. Not a real Razor parser — just enough to surface useful /// diagnostics when ExtractCodeBlock fails. diff --git a/src/ShellUI.CLI/Services/ProjectDetector.cs b/src/ShellUI.CLI/Services/ProjectDetector.cs index 27a18e3..fc15eb1 100644 --- a/src/ShellUI.CLI/Services/ProjectDetector.cs +++ b/src/ShellUI.CLI/Services/ProjectDetector.cs @@ -66,8 +66,16 @@ private static string DetectRootNamespace(XDocument doc, string projectName) .Descendants("RootNamespace") .FirstOrDefault()?.Value; - return rootNamespace ?? projectName; + return SanitizeNamespace(string.IsNullOrWhiteSpace(rootNamespace) ? projectName : rootNamespace); } + + // The .NET 8/9 templates write e.g. my-app; the compiler and Razor use my_app. + internal static string SanitizeNamespace(string value) => + string.Join(".", value.Trim().Split('.', StringSplitOptions.RemoveEmptyEntries).Select(segment => + { + var identifier = new string(segment.Select(c => char.IsLetterOrDigit(c) || c == '_' ? c : '_').ToArray()); + return char.IsDigit(identifier[0]) ? "_" + identifier : identifier; + })); } public class ProjectInfo diff --git a/src/ShellUI.Templates/Templates/AlertTemplate.cs b/src/ShellUI.Templates/Templates/AlertTemplate.cs index a297bbc..68644f2 100644 --- a/src/ShellUI.Templates/Templates/AlertTemplate.cs +++ b/src/ShellUI.Templates/Templates/AlertTemplate.cs @@ -16,7 +16,6 @@ public static class AlertTemplate }; public static string Content => @"@namespace YourProjectNamespace.Components.UI -@using YourProjectNamespace.Components.UI.Variants
@if (Icon != null) diff --git a/src/ShellUI.Templates/Templates/BadgeTemplate.cs b/src/ShellUI.Templates/Templates/BadgeTemplate.cs index 3f940b3..0e7b9ba 100644 --- a/src/ShellUI.Templates/Templates/BadgeTemplate.cs +++ b/src/ShellUI.Templates/Templates/BadgeTemplate.cs @@ -16,7 +16,6 @@ public static class BadgeTemplate }; public static string Content => @"@namespace YourProjectNamespace.Components.UI -@using YourProjectNamespace.Components.UI.Variants
@ChildContent diff --git a/src/ShellUI.Templates/Templates/ChartSeriesTemplate.cs b/src/ShellUI.Templates/Templates/ChartSeriesTemplate.cs index 674dbe8..1007518 100644 --- a/src/ShellUI.Templates/Templates/ChartSeriesTemplate.cs +++ b/src/ShellUI.Templates/Templates/ChartSeriesTemplate.cs @@ -11,7 +11,7 @@ public class ChartSeriesTemplate Description = "Individual chart series component for use in multi-series charts", Category = ComponentCategory.DataDisplay, FilePath = "ChartSeries.razor", - Dependencies = new List(), + Dependencies = new List { "chart" }, Variants = new List { "line", "bar", "area", "pie" }, Tags = new List { "chart", "series", "data", "visualization", "apexcharts" } }; diff --git a/src/ShellUI.Templates/Templates/InputTemplate.cs b/src/ShellUI.Templates/Templates/InputTemplate.cs index d2d634e..1003eb0 100644 --- a/src/ShellUI.Templates/Templates/InputTemplate.cs +++ b/src/ShellUI.Templates/Templates/InputTemplate.cs @@ -16,7 +16,6 @@ public static class InputTemplate }; public static string Content => @"@namespace YourProjectNamespace.Components.UI -@using YourProjectNamespace.Components.UI.Variants @"@namespace YourProjectNamespace.Components.UI -@using YourProjectNamespace.Components.UI.Variants + + ShellUI -

Component content

+

Card content

``` -The package also serves static assets, including `shellui.js`, from `_content/ShellUI.Components/` for components that use JavaScript interop. The package's own `SidebarProvider` retains `shellui-sidebar.js` for its runtime module; CLI-copied providers use the host-loaded `shellui.js` contract instead. - -## Component inventory - -The CLI registry contains **194 entries**: **90 direct install targets** and **104 hidden dependency entries**. The CLI displays the 90 direct targets and resolves hidden entries recursively. - -`0.3.0-rc.2` adds `typed-select`, `command-palette`, `data-picker`, `multi-select`, `tag-input`, `donut-chart`, `radar-chart`, and `radial-chart`. Use the CLI's `list` command for the complete direct-target inventory. - -## Theming with the CLI +## Themes -The CLI can apply a public [tweakcn](https://tweakcn.com) theme: +Themes come from [tweakcn](https://tweakcn.com). With the CLI, write a theme as a stylesheet and load it after `shellui-all.css`: ```bash -shellui theme apply https://tweakcn.com/themes/THEME_ID shellui theme apply https://tweakcn.com/themes/THEME_ID --emit-override wwwroot/theme.css -shellui theme update ``` -The first form updates the managed region in `wwwroot/input.css` for a Tailwind build. The override form writes standalone variables for an app using `shellui-all.css`; load that file after the precompiled stylesheet. The source URL and hash are stored in `shellui.theme.lock` for `theme update`. - -These commands require CLI `0.3.0-rc.2` or later. - -## Accessibility - -ShellUI components use Blazor and Tailwind patterns for semantics, focus, and keyboard interaction where implemented. Accessibility still depends on the component API, configuration, content, and host application; test each consuming app rather than assuming a blanket conformance level. +```html + +``` ## Documentation -- [Repository README](https://github.com/shellui-dev/shellui/blob/main/README.md) +- [README](https://github.com/shellui-dev/shellui/blob/main/README.md) - [Tailwind setup](https://github.com/shellui-dev/shellui/blob/main/docs/tailwind-setup.md) -- [Contributing guide](https://github.com/shellui-dev/shellui/blob/main/docs/CONTRIBUTING.md) -- [Historical release notes](https://github.com/shellui-dev/shellui/blob/main/docs/RELEASE_NOTES.md) +- [Release notes](https://github.com/shellui-dev/shellui/blob/main/docs/RELEASE_NOTES.md) ## License diff --git a/src/ShellUI.Templates/README.md b/src/ShellUI.Templates/README.md index d88b33d..fee70f4 100644 --- a/src/ShellUI.Templates/README.md +++ b/src/ShellUI.Templates/README.md @@ -39,7 +39,7 @@ The registry contains **194 entries**: Each entry provides content plus metadata used by the CLI, including its display name, category, description, target path, version, dependencies, and optional NuGet dependencies. The catalog also includes hidden subcomponents, variants, models, services, JavaScript, and stylesheet assets required by direct targets. -`0.3.0-rc.2` adds the direct targets `typed-select`, `command-palette`, `data-picker`, `multi-select`, `tag-input`, `donut-chart`, `radar-chart`, and `radial-chart`. +`0.3.0` added the direct targets `typed-select`, `command-palette`, `data-picker`, `multi-select`, `tag-input`, `donut-chart`, `radar-chart`, and `radial-chart`. ## Development