Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,18 @@
"Bash(\"C:\\Users\\stef\\source\\repos\\MvcFrontendKit\\src\\MvcFrontendKit\\runtimes\\win-x64\\native\\sass.bat\" \"C:\\Users\\stef\\source\\repos\\TestKit\\wwwroot\\css\\components\\notification.scss\" \"C:\\Users\\stef\\source\\repos\\TestKit\\wwwroot\\css\\components\\notification.css\" --no-source-map --no-charset)",
"Bash(\"C:\\Users\\stef\\source\\repos\\MvcFrontendKit\\src\\MvcFrontendKit\\runtimes\\win-x64\\native\\sass.bat\" --help)",
"Bash(\"C:\\Users\\stef\\source\\repos\\MvcFrontendKit\\src\\MvcFrontendKit\\runtimes\\win-x64\\native\\sass.bat\" \"C:\\Users\\stef\\source\\repos\\TestKit\\wwwroot\\css\\site.scss:C:\\Users\\stef\\source\\repos\\TestKit\\wwwroot\\css\\site.css\" \"C:\\Users\\stef\\source\\repos\\TestKit\\wwwroot\\css\\components\\notification.scss:C:\\Users\\stef\\source\\repos\\TestKit\\wwwroot\\css\\components\\notification.css\" --no-source-map --no-charset)",
"Bash(\"C:\\Users\\stef\\source\\repos\\MvcFrontendKit\\src\\MvcFrontendKit\\runtimes\\win-x64\\native\\sass.bat\" \"wwwroot/css/site.scss:wwwroot/css/test1.css\" \"wwwroot/css/Home/Index.scss:wwwroot/css/Home/test2.css\" --no-source-map --no-charset)"
"Bash(\"C:\\Users\\stef\\source\\repos\\MvcFrontendKit\\src\\MvcFrontendKit\\runtimes\\win-x64\\native\\sass.bat\" \"wwwroot/css/site.scss:wwwroot/css/test1.css\" \"wwwroot/css/Home/Index.scss:wwwroot/css/Home/test2.css\" --no-source-map --no-charset)",
"WebFetch(domain:raw.githubusercontent.com)",
"Bash(dotnet publish:*)",
"Bash(curl:*)",
"Bash(git restore:*)",
"Bash(dotnet tool:*)",
"Bash(dotnet-frontend build:*)",
"Bash(unzip:*)",
"Bash(dotnet frontend dev:*)",
"Bash(timeout 5 dotnet frontend:*)",
"Bash(timeout 8 dotnet frontend dev:*)",
"Bash(timeout 5 dotnet run:*)"
],
"deny": [],
"ask": []
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ dotnet tool install MvcFrontendKit.Cli
dotnet frontend init
```

This also patches your `.csproj` to include `frontend.config.yaml` in `dotnet publish` output. If the `.csproj` cannot be found or patched automatically, the command prints instructions for adding the entry manually.

If you don't have the CLI installed, you can copy the template from the [SPEC.md](SPEC.md#32-core-schema-overview) or let the MSBuild target auto-generate a default config on first build.

### 4. Register services in Program.cs
Expand Down
5 changes: 5 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -770,9 +770,14 @@ This helper is optional and intended for development troubleshooting. It can saf

- Creates `frontend.config.yaml` if missing, using a template identical in structure and comments to Section 3.2.
- Does not overwrite existing config unless `--force` is specified.
- After writing the YAML file, automatically patches the `.csproj` to include `frontend.config.yaml` as a `<Content>` item with `CopyToPublishDirectory="PreserveNewest"`, ensuring it is included in `dotnet publish` output.
- Finds the `.csproj` by scanning the current directory; skips patching if zero or more than one `.csproj` is found.
- Checks for an existing Content item (case-insensitive) before adding; running `init --force` twice does not duplicate the item.
- If `.csproj` patching fails for any reason, prints a warning with manual XML snippet instructions (does not fail the init command).

### 10.2 `dotnet frontend check [--verbose]`

- Validates `.csproj` includes `frontend.config.yaml` as a Content item for publish output. Reports a warning (not an error) if missing, with fix instructions.
- Loads YAML config and validates:
- `global.js` / `global.css` paths.
- `views.overrides[*].js` / `css` paths.
Expand Down
51 changes: 51 additions & 0 deletions src/MvcFrontendKit.Cli/Commands/CheckCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Text.RegularExpressions;
using System.Xml.Linq;
using MvcFrontendKit.Cli.Helpers;
using MvcFrontendKit.Configuration;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
Expand Down Expand Up @@ -68,6 +70,7 @@ public static int Execute(bool verbose, string? viewKey = null, bool checkAll =
}

// Standard check
warnings += CheckProjectFile(verbose);
errors += CheckGlobalAssets(config, verbose, validateImports);
errors += CheckViewOverrides(config, verbose, validateImports);
errors += CheckComponents(config, verbose, validateImports);
Expand Down Expand Up @@ -101,6 +104,54 @@ public static int Execute(bool verbose, string? viewKey = null, bool checkAll =
}
}

/// <summary>
/// Checks whether the .csproj includes frontend.config.yaml as a Content item for publish output.
/// Returns 1 if warning issued, 0 otherwise.
/// </summary>
private static int CheckProjectFile(bool verbose)
{
var csprojPath = CsprojHelper.FindCsprojFile(Directory.GetCurrentDirectory());

if (csprojPath == null)
{
if (verbose)
{
Console.WriteLine("Project File:");
Console.WriteLine(" ⚠ No .csproj found (skipped)");
Console.WriteLine();
}
return 0;
}

Console.WriteLine("Project File:");

try
{
var doc = XDocument.Load(csprojPath);

if (CsprojHelper.HasConfigContentItem(doc))
{
Console.WriteLine($" ✓ {Path.GetFileName(csprojPath)} includes frontend.config.yaml for publish");
}
else
{
Console.WriteLine($" ⚠ {Path.GetFileName(csprojPath)} does not include frontend.config.yaml for publish");
Console.WriteLine(" Config file will not be included in 'dotnet publish' output.");
Console.WriteLine(" Fix: Run 'dotnet frontend init --force' or add manually:");
Console.WriteLine(CsprojHelper.GetManualXmlSnippet());
Console.WriteLine();
return 1;
}
}
catch (Exception ex)
{
Console.WriteLine($" ⚠ Could not read {Path.GetFileName(csprojPath)}: {ex.Message}");
}

Console.WriteLine();
return 0;
}

/// <summary>
/// Checks a specific view by key and shows detailed diagnostic information.
/// </summary>
Expand Down
39 changes: 39 additions & 0 deletions src/MvcFrontendKit.Cli/Commands/InitCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Reflection;
using System.Xml.Linq;
using MvcFrontendKit.Cli.Helpers;

namespace MvcFrontendKit.Cli.Commands;

Expand Down Expand Up @@ -28,6 +30,9 @@ public static int Execute(bool force)
File.WriteAllText(configPath, template);

Console.WriteLine($"✓ Created frontend.config.yaml at: {configPath}");

PatchCsproj(Directory.GetCurrentDirectory());

Console.WriteLine();
Console.WriteLine("Next steps:");
Console.WriteLine(" 1. Edit frontend.config.yaml to match your project structure");
Expand All @@ -44,6 +49,40 @@ public static int Execute(bool force)
}
}

private static void PatchCsproj(string directory)
{
try
{
var csprojPath = CsprojHelper.FindCsprojFile(directory);
if (csprojPath == null)
{
Console.WriteLine();
Console.WriteLine("⚠ Could not find a .csproj file to patch.");
Console.WriteLine(" Add the following to your .csproj manually to include the config in publish output:");
Console.WriteLine(CsprojHelper.GetManualXmlSnippet());
return;
}

var doc = XDocument.Load(csprojPath);

if (!CsprojHelper.AddConfigContentItem(doc))
{
Console.WriteLine($"✓ {Path.GetFileName(csprojPath)} already includes frontend.config.yaml");
return;
}

doc.Save(csprojPath);
Console.WriteLine($"✓ Added frontend.config.yaml to {Path.GetFileName(csprojPath)}");
}
catch (Exception ex)
{
Console.WriteLine();
Console.WriteLine($"⚠ Could not patch .csproj: {ex.Message}");
Console.WriteLine(" Add the following to your .csproj manually:");
Console.WriteLine(CsprojHelper.GetManualXmlSnippet());
}
}

private static string? GetEmbeddedTemplate()
{
var assembly = Assembly.GetExecutingAssembly();
Expand Down
79 changes: 79 additions & 0 deletions src/MvcFrontendKit.Cli/Helpers/CsprojHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Xml.Linq;

namespace MvcFrontendKit.Cli.Helpers;

public static class CsprojHelper
{
private const string ConfigFileName = "frontend.config.yaml";

/// <summary>
/// Finds a single .csproj file in the given directory.
/// Returns null if zero or more than one .csproj is found.
/// </summary>
public static string? FindCsprojFile(string directory)
{
var csprojFiles = Directory.GetFiles(directory, "*.csproj");

if (csprojFiles.Length == 0)
{
return null;
}

if (csprojFiles.Length > 1)
{
Console.WriteLine($" Multiple .csproj files found in {directory}. Skipping .csproj patching.");
return null;
}

return csprojFiles[0];
}

/// <summary>
/// Checks whether the XDocument already contains a Content item for frontend.config.yaml.
/// Case-insensitive comparison on the Include attribute.
/// </summary>
public static bool HasConfigContentItem(XDocument doc)
{
var ns = doc.Root?.Name.Namespace ?? XNamespace.None;

return doc.Descendants(ns + "Content")
.Any(el => string.Equals(
el.Attribute("Include")?.Value,
ConfigFileName,
StringComparison.OrdinalIgnoreCase));
}

/// <summary>
/// Adds a Content item for frontend.config.yaml with CopyToPublishDirectory="PreserveNewest".
/// Returns true if the item was added, false if it already exists.
/// </summary>
public static bool AddConfigContentItem(XDocument doc)
{
if (HasConfigContentItem(doc))
{
return false;
}

var ns = doc.Root?.Name.Namespace ?? XNamespace.None;

var itemGroup = new XElement(ns + "ItemGroup",
new XElement(ns + "Content",
new XAttribute("Include", ConfigFileName),
new XAttribute("CopyToPublishDirectory", "PreserveNewest")));

doc.Root!.Add(itemGroup);
return true;
}

/// <summary>
/// Returns the XML snippet for manual .csproj patching instructions.
/// </summary>
public static string GetManualXmlSnippet()
{
return """
<ItemGroup>
<Content Include="frontend.config.yaml" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
""";
}
}
2 changes: 1 addition & 1 deletion src/MvcFrontendKit.Cli/MvcFrontendKit.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<Nullable>enable</Nullable>
<PackAsTool>true</PackAsTool>
<ToolCommandName>dotnet-frontend</ToolCommandName>
<Version>1.0.0</Version>
<Version>1.1.0</Version>
<Authors>stef-k</Authors>
<Description>CLI tool for MvcFrontendKit - compile TypeScript/SCSS during development, validate config and check assets. Uses esbuild and Dart Sass.</Description>
<PackageTags>aspnetcore;mvc;razor;bundling;esbuild;sass;scss;typescript;frontend;cli</PackageTags>
Expand Down
2 changes: 1 addition & 1 deletion src/MvcFrontendKit/MvcFrontendKit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>1.0.0</Version>
<Version>1.1.0</Version>
<Authors>stef-k</Authors>
<Description>Node-free frontend bundling toolkit for ASP.NET Core MVC / Razor applications. Uses esbuild for JS/TS bundling and Dart Sass for SCSS compilation.</Description>
<PackageTags>aspnetcore;mvc;razor;bundling;esbuild;sass;scss;typescript;frontend</PackageTags>
Expand Down
2 changes: 1 addition & 1 deletion src/MvcFrontendKit/Services/FrontendConfigProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ private FrontendConfig LoadConfig()

if (!File.Exists(configPath))
{
_logger.LogWarning(
_logger.LogDebug(
"Frontend config file not found at {ConfigPath}. Using default configuration. " +
"Run 'dotnet frontend init' to generate a config file.",
configPath);
Expand Down
Loading
Loading