From 46b3fde52e8f9a45e418127e4024780ab5947606 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Wed, 17 Jun 2026 18:31:00 +0200 Subject: [PATCH 1/6] feat(ci): enhance smoke tests for shellui init validation Added assertions in the CI workflow to verify that the `shellui init` command correctly patches the `App.razor` file and writes the full theme to `input.css`. This ensures that the initialization process produces a working host with the expected configurations and theme variables, improving the reliability of the scaffolding process. --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3350350..3e9c1ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,20 @@ jobs: dotnet new blazor -o SmokeApp --no-restore cd SmokeApp shellui init --tailwind standalone --yes + + # Assert init produced a working host: App.razor patched with render mode, + # theme bootstrap, and shellui.js script tag. + grep -q 'HeadOutlet @rendermode="InteractiveServer"' Components/App.razor || (echo "init did not patch HeadOutlet @rendermode"; exit 1) + grep -q 'Routes @rendermode="InteractiveServer"' Components/App.razor || (echo "init did not patch Routes @rendermode"; exit 1) + grep -q 'ShellUI theme bootstrap' Components/App.razor || (echo "init did not inject theme bootstrap"; exit 1) + grep -q '' Components/App.razor || (echo "init did not inject shellui.js script tag"; exit 1) + grep -q 'shellui-sidebar.js' Components/App.razor && (echo "init incorrectly injected shellui-sidebar.js script tag (sidebar JS is dynamically imported)"; exit 1) || true + + # Assert input.css has the full theme, not just @import "tailwindcss"; + grep -q '@theme inline' wwwroot/input.css || (echo "init did not write full theme to input.css"; exit 1) + 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` From eb45db36252ad9d770559a5b1c982691c453fe66 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Wed, 17 Jun 2026 18:35:02 +0200 Subject: [PATCH 2/6] feat(tests): add InitBootstrapTests for validating Razor file rewrites Introduced a new test class, InitBootstrapTests, to validate the functionality of the InitService's methods for rewriting Razor files. The tests ensure that the `RewriteAppRazor` and `RewriteWasmIndexHtml` methods correctly inject necessary scripts and theme elements, maintain idempotency, and preserve existing render modes. This addition enhances test coverage for the initialization process in ShellUI. --- ShellUI.Tests/InitBootstrapTests.cs | 119 ++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 ShellUI.Tests/InitBootstrapTests.cs diff --git a/ShellUI.Tests/InitBootstrapTests.cs b/ShellUI.Tests/InitBootstrapTests.cs new file mode 100644 index 0000000..850f790 --- /dev/null +++ b/ShellUI.Tests/InitBootstrapTests.cs @@ -0,0 +1,119 @@ +using ShellUI.CLI.Services; +using Xunit; + +namespace ShellUI.Tests; + +public class InitBootstrapTests +{ + private const string FreshAppRazor = @" + + + + + + + + + + + + + + + + + +"; + + [Fact] + public void RewriteAppRazor_AddsRenderModeToHeadOutletAndRoutes() + { + var result = InitService.RewriteAppRazor(FreshAppRazor); + + Assert.Contains(@"", result); + Assert.Contains(@"", result); + } + + [Fact] + public void RewriteAppRazor_InjectsThemeBootstrapInHead() + { + var result = InitService.RewriteAppRazor(FreshAppRazor); + + Assert.Contains("ShellUI theme bootstrap", result); + Assert.Contains("classList.add('dark')", result); + // Theme script must be inside , before . + var themeIdx = result.IndexOf("ShellUI theme bootstrap"); + var headCloseIdx = result.IndexOf(""); + Assert.True(themeIdx > 0 && themeIdx < headCloseIdx); + } + + [Fact] + public void RewriteAppRazor_InjectsShelluiJsBeforeBlazorScript() + { + var result = InitService.RewriteAppRazor(FreshAppRazor); + + var shelluiIdx = result.IndexOf(@""); + var blazorIdx = result.IndexOf(@" + +"; + + var result = InitService.RewriteWasmIndexHtml(indexHtml); + + Assert.Contains("ShellUI theme bootstrap", result); + var shelluiIdx = result.IndexOf(@""); + var blazorIdx = result.IndexOf(@""; + + var once = InitService.RewriteWasmIndexHtml(indexHtml); + var twice = InitService.RewriteWasmIndexHtml(once); + + Assert.Equal(once, twice); + } +} From 5283cf16f9ad4a75a970d978c5fc13ea28a554cd Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Wed, 17 Jun 2026 18:35:52 +0200 Subject: [PATCH 3/6] feat(init): implement BootstrapHostAsync for patching App.razor and index.html Added the BootstrapHostAsync method to the InitService, which handles the patching of Components/App.razor and wwwroot/index.html for Blazor applications. This method ensures the correct injection of theme bootstrap scripts and the shellui.js script tag, enhancing the initialization process. Updated the status messages to reflect these changes and improved the overall user experience during setup. --- src/ShellUI.CLI/Services/InitService.cs | 106 +++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/src/ShellUI.CLI/Services/InitService.cs b/src/ShellUI.CLI/Services/InitService.cs index 4b356dc..2f1b754 100644 --- a/src/ShellUI.CLI/Services/InitService.cs +++ b/src/ShellUI.CLI/Services/InitService.cs @@ -1,6 +1,7 @@ using ShellUI.Core.Models; using ShellUI.Templates; using System.Text.Json; +using System.Text.RegularExpressions; using Spectre.Console; namespace ShellUI.CLI.Services; @@ -147,6 +148,10 @@ await AnsiConsole.Status() await SetupTailwindStandaloneAsync(); } + // Step 6.5: Patch App.razor / index.html — render mode + theme bootstrap + shellui.js + ctx.Status("Wiring up theme and render mode..."); + await BootstrapHostAsync(projectInfo); + // Step 7: Create MSBuild targets file ctx.Status("Setting up MSBuild integration..."); var buildPath = Path.Combine(Directory.GetCurrentDirectory(), "Build"); @@ -181,7 +186,6 @@ await AnsiConsole.Status() AnsiConsole.MarkupLine("\n[blue]Next steps:[/]"); AnsiConsole.MarkupLine(" [dim]1. Add components:[/] dotnet shellui add button"); AnsiConsole.MarkupLine(" [dim]2. Browse all:[/] dotnet shellui list"); - AnsiConsole.MarkupLine(" [dim]3. CopyButton/FileUpload/Command:[/] Add [yellow][/] before Blazor script in App.razor or index.html"); } private static async Task SetupTailwindNpmAsync() @@ -435,6 +439,106 @@ private static async Task UpdateProjectFileAsync(string projectFilePath, string } } + private static async Task BootstrapHostAsync(ProjectInfo projectInfo) + { + var cwd = Directory.GetCurrentDirectory(); + + // Blazor Web App / Server / SSR: patch Components/App.razor + var appRazor = Path.Combine(cwd, "Components", "App.razor"); + if (File.Exists(appRazor)) + { + var original = await File.ReadAllTextAsync(appRazor); + var patched = RewriteAppRazor(original); + if (patched != original) + { + await File.WriteAllTextAsync(appRazor, patched); + AnsiConsole.MarkupLine("[green]Patched:[/] Components/App.razor"); + } + return; + } + + // Blazor WASM (standalone): patch wwwroot/index.html instead — no Routes/HeadOutlet + // render-mode pattern there; just inject theme bootstrap + shellui.js script tag. + var indexHtml = Path.Combine(cwd, "wwwroot", "index.html"); + if (File.Exists(indexHtml)) + { + var original = await File.ReadAllTextAsync(indexHtml); + var patched = RewriteWasmIndexHtml(original); + if (patched != original) + { + await File.WriteAllTextAsync(indexHtml, patched); + AnsiConsole.MarkupLine("[green]Patched:[/] wwwroot/index.html"); + } + return; + } + + AnsiConsole.MarkupLine("[yellow]No Components/App.razor or wwwroot/index.html found — skipped host bootstrap.[/]"); + } + + // Idempotent: a second `shellui init` won't double-inject. Tags that already + // carry @rendermode or any other attribute aren't matched, so user customizations + // are preserved (they'll need to set @rendermode manually). + internal static string RewriteAppRazor(string content) + { + content = Regex.Replace(content, @"", @""); + content = Regex.Replace(content, @"", @""); + + // 3. Theme bootstrap in — sets `dark` class before paint to avoid FOUC. + if (!content.Contains("ShellUI theme bootstrap")) + { + const string themeScript = +@" +"; + content = Regex.Replace(content, @"", themeScript + "", RegexOptions.IgnoreCase); + } + + // 4. immediately before blazor.web.js — provides + // window.ShellUI.* (addClassToDocument, focusElement, copyToClipboard, …) for + // ThemeToggle, CopyButton, InputOTP, FileUpload, Command. + if (!content.Contains("shellui.js")) + { + content = Regex.Replace( + content, + @"(\n $1"); + } + + return content; + } + + internal static string RewriteWasmIndexHtml(string content) + { + if (!content.Contains("ShellUI theme bootstrap")) + { + const string themeScript = +@" +"; + content = Regex.Replace(content, @"", themeScript + "", RegexOptions.IgnoreCase); + } + + if (!content.Contains("shellui.js")) + { + content = Regex.Replace( + content, + @"(\n $1"); + } + + return content; + } + private static void RemoveBootstrapFiles() { try From 0be226f31f53770f8ab2920c844645f5d261c3fe Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Wed, 17 Jun 2026 18:36:22 +0200 Subject: [PATCH 4/6] feat(tests): add ShellUI.CLI project reference and expose internals to tests Added a project reference for ShellUI.CLI in the ShellUI.Tests project to facilitate testing. Additionally, made internals of ShellUI.CLI visible to ShellUI.Tests, enabling comprehensive unit testing of internal components and improving test coverage. --- ShellUI.Tests/ShellUI.Tests.csproj | 1 + src/ShellUI.CLI/ShellUI.CLI.csproj | 1 + 2 files changed, 2 insertions(+) diff --git a/ShellUI.Tests/ShellUI.Tests.csproj b/ShellUI.Tests/ShellUI.Tests.csproj index aa45fab..932a68f 100644 --- a/ShellUI.Tests/ShellUI.Tests.csproj +++ b/ShellUI.Tests/ShellUI.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/src/ShellUI.CLI/ShellUI.CLI.csproj b/src/ShellUI.CLI/ShellUI.CLI.csproj index 7dab43e..58aa584 100644 --- a/src/ShellUI.CLI/ShellUI.CLI.csproj +++ b/src/ShellUI.CLI/ShellUI.CLI.csproj @@ -32,6 +32,7 @@ + From 476d79cc2480a471ee5795b9dc05211306ea4a3a Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Wed, 17 Jun 2026 18:36:43 +0200 Subject: [PATCH 5/6] feat(css): refactor CSS templates to streamline theme management Updated the CssTemplates class to replace the InputCss property with FullThemeCss, introducing a new InputCssNpm property. Enhanced TailwindConfigJsNpm with comments for clarity on theme handling and removed unnecessary theme configurations. This refactor improves the overall structure and maintainability of the CSS templates, aligning with Tailwind v4's requirements for theme management. --- src/ShellUI.Templates/CssTemplates.cs | 247 +++++++++++++++++++++----- 1 file changed, 200 insertions(+), 47 deletions(-) diff --git a/src/ShellUI.Templates/CssTemplates.cs b/src/ShellUI.Templates/CssTemplates.cs index 949f959..e8143d2 100644 --- a/src/ShellUI.Templates/CssTemplates.cs +++ b/src/ShellUI.Templates/CssTemplates.cs @@ -2,8 +2,8 @@ namespace ShellUI.Templates; public static class CssTemplates { - public static string InputCss => @"@import ""tailwindcss""; -"; + public static string InputCss => FullThemeCss; + public static string InputCssNpm => FullThemeCss; public static string AppCss => @"/* * ShellUI - Blazor Component Library @@ -26,6 +26,8 @@ public static class CssTemplates } "; + // Tailwind v4 reads the theme via `@theme inline` inside input.css — no need + // to map CSS vars in the config file. Customize the `content` globs only. public static string TailwindConfigJsNpm => @"/** @type {import('tailwindcss').Config} */ module.exports = { content: [ @@ -33,54 +35,205 @@ public static class CssTemplates './Pages/**/*.{razor,html,cshtml}', ], darkMode: 'class', - theme: { - extend: { - colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', - }, - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)', - }, - }, - }, plugins: [], } "; - public static string InputCssNpm => @"@import ""tailwindcss""; + // Default theme: light + dark CSS variables, @theme inline mapping for Tailwind v4, + // and base animation keyframes used by the Loading component. Tweakcn-compatible — + // users can paste a new :root / .dark block over this to retheme. + private const string FullThemeCss = @"@import ""tailwindcss""; +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(0.9900 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --popover: oklch(0.9900 0 0); + --popover-foreground: oklch(0 0 0); + --primary: oklch(0 0 0); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.9400 0 0); + --secondary-foreground: oklch(0 0 0); + --muted: oklch(0.9700 0 0); + --muted-foreground: oklch(0.4400 0 0); + --accent: oklch(0.9400 0 0); + --accent-foreground: oklch(0 0 0); + --destructive: oklch(0.6300 0.1900 23.0300); + --destructive-foreground: oklch(1 0 0); + --border: oklch(0.9200 0 0); + --input: oklch(0.9400 0 0); + --ring: oklch(0 0 0); + --chart-1: oklch(0.8100 0.1700 75.3500); + --chart-2: oklch(0.5500 0.2200 264.5300); + --chart-3: oklch(0.7200 0 0); + --chart-4: oklch(0.9200 0 0); + --chart-5: oklch(0.5600 0 0); + --sidebar: oklch(0.9900 0 0); + --sidebar-foreground: oklch(0 0 0); + --sidebar-primary: oklch(0 0 0); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.9400 0 0); + --sidebar-accent-foreground: oklch(0 0 0); + --sidebar-border: oklch(0.9400 0 0); + --sidebar-ring: oklch(0 0 0); + --font-sans: Geist, sans-serif; + --font-serif: Georgia, serif; + --font-mono: Geist Mono, monospace; + --radius: 0.5rem; + --shadow-x: 0px; + --shadow-y: 1px; + --shadow-blur: 2px; + --shadow-spread: 0px; + --shadow-opacity: 0.18; + --shadow-color: hsl(0 0% 0%); + --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09); + --shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09); + --shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18); + --shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18); + --shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18); + --shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18); + --shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18); + --shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45); + --tracking-normal: 0em; + --spacing: 0.25rem; +} + +.dark { + --background: oklch(0 0 0); + --foreground: oklch(1 0 0); + --card: oklch(0.1400 0 0); + --card-foreground: oklch(1 0 0); + --popover: oklch(0.1800 0 0); + --popover-foreground: oklch(1 0 0); + --primary: oklch(1 0 0); + --primary-foreground: oklch(0 0 0); + --secondary: oklch(0.2500 0 0); + --secondary-foreground: oklch(1 0 0); + --muted: oklch(0.2300 0 0); + --muted-foreground: oklch(0.7200 0 0); + --accent: oklch(0.3200 0 0); + --accent-foreground: oklch(1 0 0); + --destructive: oklch(0.6900 0.2000 23.9100); + --destructive-foreground: oklch(0 0 0); + --border: oklch(0.2600 0 0); + --input: oklch(0.3200 0 0); + --ring: oklch(0.7200 0 0); + --chart-1: oklch(0.8100 0.1700 75.3500); + --chart-2: oklch(0.5800 0.2100 260.8400); + --chart-3: oklch(0.5600 0 0); + --chart-4: oklch(0.4400 0 0); + --chart-5: oklch(0.9200 0 0); + --sidebar: oklch(0.1800 0 0); + --sidebar-foreground: oklch(1 0 0); + --sidebar-primary: oklch(1 0 0); + --sidebar-primary-foreground: oklch(0 0 0); + --sidebar-accent: oklch(0.3200 0 0); + --sidebar-accent-foreground: oklch(1 0 0); + --sidebar-border: oklch(0.3200 0 0); + --sidebar-ring: oklch(0.7200 0 0); + --font-sans: Geist, sans-serif; + --font-serif: Georgia, serif; + --font-mono: Geist Mono, monospace; + --radius: 0.5rem; + --shadow-color: hsl(0 0% 0%); + --shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09); + --shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09); + --shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18); + --shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18); + --shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18); + --shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18); + --shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18); + --shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); + --font-serif: var(--font-serif); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + + --shadow-2xs: var(--shadow-2xs); + --shadow-xs: var(--shadow-xs); + --shadow-sm: var(--shadow-sm); + --shadow: var(--shadow); + --shadow-md: var(--shadow-md); + --shadow-lg: var(--shadow-lg); + --shadow-xl: var(--shadow-xl); + --shadow-2xl: var(--shadow-2xl); +} + +@layer base { + * { + border-color: var(--border); + } + body { + background-color: var(--background); + color: var(--foreground); + } +} + +html, body, :host { + font-family: var(--font-sans); +} + +/* Animation keyframes used by the Loading component variants */ +@keyframes bars { + 0%, 100% { transform: scaleY(0.4); opacity: 0.7; } + 50% { transform: scaleY(1); opacity: 1; } +} + +@keyframes bars-vertical { + 0%, 100% { transform: scaleY(0.4); opacity: 0.7; } + 50% { transform: scaleY(1); opacity: 1; } +} + +@keyframes bars-pulse { + 0%, 100% { transform: scaleY(0.3); opacity: 0.5; } + 50% { transform: scaleY(1); opacity: 1; } +} + +@keyframes orbit { + 0% { transform: translate(-50%, 0) rotate(0deg) translateX(calc((var(--size, 1rem) - 0.5rem) / 2)) rotate(0deg); } + 100% { transform: translate(-50%, 0) rotate(360deg) translateX(calc((var(--size, 1rem) - 0.5rem) / 2)) rotate(-360deg); } +} "; } From febd171b53febc89cab5ea28174b813e502468d0 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Wed, 17 Jun 2026 18:46:50 +0200 Subject: [PATCH 6/6] feat(tests): enhance InitBootstrapTests to validate script tag handling Updated InitBootstrapTests to include a new test for handling bare Blazor script tags in the RewriteAppRazor method. This ensures compatibility with older templates that do not use the @Assets[] wrapper. Additionally, modified the InitService to support both modern and bare script tag formats, improving the robustness of the Razor file rewriting process. --- ShellUI.Tests/InitBootstrapTests.cs | 19 +++++++++++++++++-- src/ShellUI.CLI/Services/InitService.cs | 7 ++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/ShellUI.Tests/InitBootstrapTests.cs b/ShellUI.Tests/InitBootstrapTests.cs index 850f790..b78de84 100644 --- a/ShellUI.Tests/InitBootstrapTests.cs +++ b/ShellUI.Tests/InitBootstrapTests.cs @@ -5,6 +5,9 @@ namespace ShellUI.Tests; public class InitBootstrapTests { + // Mirrors the App.razor that `dotnet new blazor` (net9) produces, including the + // @Assets[] asset-fingerprinting wrapper around the Blazor script. The wrapper + // is what tripped the smoke test on the first CI run of this branch. private const string FreshAppRazor = @" @@ -19,7 +22,7 @@ public class InitBootstrapTests - + @@ -53,12 +56,24 @@ public void RewriteAppRazor_InjectsShelluiJsBeforeBlazorScript() var result = InitService.RewriteAppRazor(FreshAppRazor); var shelluiIdx = result.IndexOf(@""); - var blazorIdx = result.IndexOf(@""; + + var result = InitService.RewriteAppRazor(bare); + + Assert.Contains(@"", result); + Assert.True(result.IndexOf(@"shellui.js") < result.IndexOf(@"blazor.web.js")); + } + [Fact] public void RewriteAppRazor_IsIdempotent() { diff --git a/src/ShellUI.CLI/Services/InitService.cs b/src/ShellUI.CLI/Services/InitService.cs index 2f1b754..8cd1906 100644 --- a/src/ShellUI.CLI/Services/InitService.cs +++ b/src/ShellUI.CLI/Services/InitService.cs @@ -500,12 +500,13 @@ internal static string RewriteAppRazor(string content) // 4. immediately before blazor.web.js — provides // window.ShellUI.* (addClassToDocument, focusElement, copyToClipboard, …) for - // ThemeToggle, CopyButton, InputOTP, FileUpload, Command. + // ThemeToggle, CopyButton, InputOTP, FileUpload, Command. The pattern matches + // both the modern `@Assets["_framework/blazor.web.js"]` and the bare form. if (!content.Contains("shellui.js")) { content = Regex.Replace( content, - @"(]*_framework/blazor\.web\.js[^>]*>)", "\n $1"); } @@ -532,7 +533,7 @@ internal static string RewriteWasmIndexHtml(string content) { content = Regex.Replace( content, - @"(]*_framework/blazor\.webassembly\.js[^>]*>)", "\n $1"); }