From d1dff67b283b29fc6b8edd499e2ad6477059a1fa Mon Sep 17 00:00:00 2001 From: botoddly <250804054+botoddly@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:25:31 +0100 Subject: [PATCH 1/4] Add failing test proving generated .cs files are not compiled --- .../Fluidify.Tests.Functional/CompileTests.cs | 70 +++++++++++++++++++ .../Fixtures/CompileApp/CompileApp.csproj | 16 +++++ .../Fixtures/CompileApp/Models/Greeter.cs | 6 ++ .../CompileApp/Models/Greeter.cs.fluid | 6 ++ .../Fixtures/CompileApp/Program.cs | 3 + .../Fixtures/CompileApp/nuget.config | 8 +++ 6 files changed, 109 insertions(+) create mode 100644 tests/Fluidify.Tests.Functional/CompileTests.cs create mode 100644 tests/Fluidify.Tests.Functional/Fixtures/CompileApp/CompileApp.csproj create mode 100644 tests/Fluidify.Tests.Functional/Fixtures/CompileApp/Models/Greeter.cs create mode 100644 tests/Fluidify.Tests.Functional/Fixtures/CompileApp/Models/Greeter.cs.fluid create mode 100644 tests/Fluidify.Tests.Functional/Fixtures/CompileApp/Program.cs create mode 100644 tests/Fluidify.Tests.Functional/Fixtures/CompileApp/nuget.config diff --git a/tests/Fluidify.Tests.Functional/CompileTests.cs b/tests/Fluidify.Tests.Functional/CompileTests.cs new file mode 100644 index 0000000..9f019b9 --- /dev/null +++ b/tests/Fluidify.Tests.Functional/CompileTests.cs @@ -0,0 +1,70 @@ +using System.Diagnostics; +using NUnit.Framework; + +namespace Fluidify.Tests.Functional; + +[TestFixture] +public class CompileTests +{ + private static readonly string SolutionRoot = Path.GetFullPath( + Path.Combine(TestContext.CurrentContext.TestDirectory, "..", "..", "..", "..", "..")); + + private static readonly string FluidifyProject = Path.Combine( + SolutionRoot, "src", "Fluidify", "Fluidify.csproj"); + + private static readonly string CompileAppDir = Path.Combine( + SolutionRoot, "tests", "Fluidify.Tests.Functional", "Fixtures", "CompileApp"); + + [OneTimeSetUp] + public async Task PackAndBuildFluidify() + { + // Clear any cached Fluidify package from the global NuGet cache + (int ExitCode, string Output) localsResult = await RunDotnet("nuget locals global-packages --list"); + string globalPackagesDir = localsResult.Output + .Replace("global-packages:", "") + .Replace("info :", "") + .Trim(); + string fluidifyCache = Path.Combine(globalPackagesDir, "fluidify"); + if (Directory.Exists(fluidifyCache)) + Directory.Delete(fluidifyCache, true); + + // Build Fluidify (GeneratePackageOnBuild produces the nupkg) + (int ExitCode, string Output) buildResult = await RunDotnet( + $"build \"{FluidifyProject}\" -c Release"); + Assert.That(buildResult.ExitCode, Is.EqualTo(0), + $"Fluidify build failed:\n{buildResult.Output}"); + } + + [Test] + public async Task GeneratedCsFile_IsCompiledSuccessfully() + { + string generatedFile = Path.Combine(CompileAppDir, "Models", "Greeter.cs"); + + // Delete generated file to simulate a clean build + if (File.Exists(generatedFile)) + File.Delete(generatedFile); + + string compileAppProject = Path.Combine(CompileAppDir, "CompileApp.csproj"); + (int ExitCode, string Output) result = await RunDotnet($"build \"{compileAppProject}\" --force"); + + Assert.That(result.ExitCode, Is.EqualTo(0), + $"CompileApp build failed — generated .cs file was not included in compilation:\n{result.Output}"); + } + + private static async Task<(int ExitCode, string Output)> RunDotnet(string arguments) + { + ProcessStartInfo psi = new ProcessStartInfo("dotnet", arguments) + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + + using Process process = Process.Start(psi)!; + string stdout = await process.StandardOutput.ReadToEndAsync(); + string stderr = await process.StandardError.ReadToEndAsync(); + await process.WaitForExitAsync(); + + return (process.ExitCode, stdout + stderr); + } +} diff --git a/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/CompileApp.csproj b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/CompileApp.csproj new file mode 100644 index 0000000..bcbeec8 --- /dev/null +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/CompileApp.csproj @@ -0,0 +1,16 @@ + + + + net9.0 + Exe + + + + + + + + + + + diff --git a/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/Models/Greeter.cs b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/Models/Greeter.cs new file mode 100644 index 0000000..2b3bf12 --- /dev/null +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/Models/Greeter.cs @@ -0,0 +1,6 @@ +namespace CompileApp.Models; + +public static class Greeter +{ + public static string Message => "HELLO, World!"; +} diff --git a/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/Models/Greeter.cs.fluid b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/Models/Greeter.cs.fluid new file mode 100644 index 0000000..518eac6 --- /dev/null +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/Models/Greeter.cs.fluid @@ -0,0 +1,6 @@ +namespace CompileApp.Models; + +public static class Greeter +{ + public static string Message => "{{ Greeting }}, World!"; +} diff --git a/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/Program.cs b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/Program.cs new file mode 100644 index 0000000..7c823e9 --- /dev/null +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/Program.cs @@ -0,0 +1,3 @@ +using CompileApp.Models; + +Console.WriteLine(Greeter.Message); diff --git a/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/nuget.config b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/nuget.config new file mode 100644 index 0000000..0f2f46b --- /dev/null +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/nuget.config @@ -0,0 +1,8 @@ + + + + + + + + From b67ae6c7ad159a01dbe10f4ccceab6943479611b Mon Sep 17 00:00:00 2001 From: botoddly <250804054+botoddly@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:46:31 +0100 Subject: [PATCH 2/4] Auto-include generated .cs files in compilation with Compile="false" opt-out --- README.md | 22 ++++++++++++++----- src/Fluidify/FluidifyTask.cs | 3 ++- src/Fluidify/build/Fluidify.targets | 14 ++++++++++++ .../Fluidify.Tests.Functional/CompileTests.cs | 21 ++++++++++++++++++ .../Fixtures/CompileApp/CompileApp.csproj | 1 + .../CompileFalseApp/CompileFalseApp.csproj | 17 ++++++++++++++ .../CompileFalseApp/Models/Greeter.cs | 6 +++++ .../CompileFalseApp/Models/Greeter.cs.fluid | 6 +++++ .../Fixtures/CompileFalseApp/Program.cs | 3 +++ .../Fixtures/CompileFalseApp/nuget.config | 8 +++++++ 10 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/CompileFalseApp.csproj create mode 100644 tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/Models/Greeter.cs create mode 100644 tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/Models/Greeter.cs.fluid create mode 100644 tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/Program.cs create mode 100644 tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/nuget.config diff --git a/README.md b/README.md index 2f4a941..60db611 100644 --- a/README.md +++ b/README.md @@ -16,25 +16,35 @@ Add `` items to your project file. Each item points to a `.fluid` temp ```xml - + ``` Templates use standard [Liquid syntax](https://shopify.github.io/liquid/): -**Models/Greeting.cs.fluid** +**WelcomeMessage.cs.fluid** ```liquid -public static class Greeting +public static class WelcomeMessage { - public const string Value = "{{ Greeting }}, World!"; + public const string Text = "Welcome, {{ Name }}!"; } ``` -Building the project renders each template before compilation. The output path is inferred by stripping the `.fluid` extension, so `Models/Greeting.cs.fluid` produces `Models/Greeting.cs`. +Building the project renders each template before compilation. The output path is inferred by stripping the `.fluid` extension, so `WelcomeMessage.cs.fluid` produces `WelcomeMessage.cs`. Fluidify works with any text format — C#, JSON, HTML, YAML, or anything else. The `.fluid` extension is just a convention; the template itself is plain text with Liquid tags. +### Including generated C# in compilation + +By default, generated `.cs` files are not automatically compiled — MSBuild's SDK glob evaluates before templates are rendered. Add `Compile="true"` to include the output in compilation: + +```xml + + + +``` + ### Custom output path Use the `Destination` metadata to write the output to a different location: @@ -55,7 +65,7 @@ Relative paths are resolved from the project directory. Directories are created Fluidify registers an MSBuild target that runs before `CoreCompile`. For each `` item it: 1. Parses the `.fluid` file using the Fluid template engine -2. Passes all item metadata (except `Destination`) as template variables +2. Passes all item metadata (except `Destination` and `Compile`) as template variables 3. Writes the rendered output to the inferred or specified destination MSBuild tracks input and output timestamps, so templates are only re-rendered when the source file changes. diff --git a/src/Fluidify/FluidifyTask.cs b/src/Fluidify/FluidifyTask.cs index f26e826..005efef 100644 --- a/src/Fluidify/FluidifyTask.cs +++ b/src/Fluidify/FluidifyTask.cs @@ -47,7 +47,8 @@ public override bool Execute() foreach (DictionaryEntry entry in item.CloneCustomMetadata()) { string key = entry.Key.ToString(); - if (!string.Equals(key, "Destination", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(key, "Destination", StringComparison.OrdinalIgnoreCase) && + !string.Equals(key, "Compile", StringComparison.OrdinalIgnoreCase)) { context.SetValue(key, entry.Value?.ToString() ?? ""); } diff --git a/src/Fluidify/build/Fluidify.targets b/src/Fluidify/build/Fluidify.targets index 9e0408e..df4c33d 100644 --- a/src/Fluidify/build/Fluidify.targets +++ b/src/Fluidify/build/Fluidify.targets @@ -26,4 +26,18 @@ + + + + + + + + diff --git a/tests/Fluidify.Tests.Functional/CompileTests.cs b/tests/Fluidify.Tests.Functional/CompileTests.cs index 9f019b9..92e2839 100644 --- a/tests/Fluidify.Tests.Functional/CompileTests.cs +++ b/tests/Fluidify.Tests.Functional/CompileTests.cs @@ -35,6 +35,9 @@ public async Task PackAndBuildFluidify() $"Fluidify build failed:\n{buildResult.Output}"); } + private static readonly string CompileFalseAppDir = Path.Combine( + SolutionRoot, "tests", "Fluidify.Tests.Functional", "Fixtures", "CompileFalseApp"); + [Test] public async Task GeneratedCsFile_IsCompiledSuccessfully() { @@ -51,6 +54,24 @@ public async Task GeneratedCsFile_IsCompiledSuccessfully() $"CompileApp build failed — generated .cs file was not included in compilation:\n{result.Output}"); } + [Test] + public async Task GeneratedCsFile_CompileFalse_IsExcludedFromCompilation() + { + string generatedFile = Path.Combine(CompileFalseAppDir, "Models", "Greeter.cs"); + + // Delete generated file to simulate a clean build + if (File.Exists(generatedFile)) + File.Delete(generatedFile); + + string project = Path.Combine(CompileFalseAppDir, "CompileFalseApp.csproj"); + (int ExitCode, string Output) result = await RunDotnet($"build \"{project}\" --force"); + + Assert.That(result.ExitCode, Is.Not.EqualTo(0), + $"Build should have failed — Compile=\"false\" should exclude the generated .cs from compilation:\n{result.Output}"); + Assert.That(result.Output, Does.Contain("CS0246").Or.Contains("CS0103"), + "Expected a missing type/namespace compiler error"); + } + private static async Task<(int ExitCode, string Output)> RunDotnet(string arguments) { ProcessStartInfo psi = new ProcessStartInfo("dotnet", arguments) diff --git a/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/CompileApp.csproj b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/CompileApp.csproj index bcbeec8..1164d7d 100644 --- a/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/CompileApp.csproj +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/CompileApp.csproj @@ -3,6 +3,7 @@ net9.0 Exe + enable diff --git a/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/CompileFalseApp.csproj b/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/CompileFalseApp.csproj new file mode 100644 index 0000000..d32b896 --- /dev/null +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/CompileFalseApp.csproj @@ -0,0 +1,17 @@ + + + + net9.0 + Exe + enable + + + + + + + + + + + diff --git a/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/Models/Greeter.cs b/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/Models/Greeter.cs new file mode 100644 index 0000000..6036187 --- /dev/null +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/Models/Greeter.cs @@ -0,0 +1,6 @@ +namespace CompileFalseApp.Models; + +public static class Greeter +{ + public static string Message => "HELLO, World!"; +} diff --git a/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/Models/Greeter.cs.fluid b/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/Models/Greeter.cs.fluid new file mode 100644 index 0000000..c287991 --- /dev/null +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/Models/Greeter.cs.fluid @@ -0,0 +1,6 @@ +namespace CompileFalseApp.Models; + +public static class Greeter +{ + public static string Message => "{{ Greeting }}, World!"; +} diff --git a/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/Program.cs b/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/Program.cs new file mode 100644 index 0000000..9f3ac1d --- /dev/null +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/Program.cs @@ -0,0 +1,3 @@ +using CompileFalseApp.Models; + +Console.WriteLine(Greeter.Message); diff --git a/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/nuget.config b/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/nuget.config new file mode 100644 index 0000000..0f2f46b --- /dev/null +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/nuget.config @@ -0,0 +1,8 @@ + + + + + + + + From 82845c01ed661535a3c9a73cbf1c8ef84299b734 Mon Sep 17 00:00:00 2001 From: botoddly <250804054+botoddly@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:49:55 +0100 Subject: [PATCH 3/4] Add comment explaining Remove+Include pattern in AddFluidifyCompileItems --- src/Fluidify/build/Fluidify.targets | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Fluidify/build/Fluidify.targets b/src/Fluidify/build/Fluidify.targets index df4c33d..b193e43 100644 --- a/src/Fluidify/build/Fluidify.targets +++ b/src/Fluidify/build/Fluidify.targets @@ -30,7 +30,9 @@ DependsOnTargets="ProcessFluidTemplates" Condition="'@(Fluidify)' != ''"> - + From 4eeb9ddeb158adb12a2da223397ca8738eaa40f0 Mon Sep 17 00:00:00 2001 From: botoddly <250804054+botoddly@users.noreply.github.com> Date: Thu, 5 Mar 2026 06:41:38 +0100 Subject: [PATCH 4/4] Address PR #7 review comments --- README.md | 14 +++++++++++--- src/Fluidify/Fluidify.csproj | 2 +- src/Fluidify/FluidifyTask.cs | 7 +++++-- src/Fluidify/build/Fluidify.targets | 18 ++++++++++-------- .../Fixtures/CompileApp/CompileApp.csproj | 3 ++- .../CompileFalseApp/CompileFalseApp.csproj | 3 ++- .../Fixtures/SampleApp/SampleApp.csproj | 3 ++- 7 files changed, 33 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 60db611..65c7374 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,21 @@ Building the project renders each template before compilation. The output path i Fluidify works with any text format — C#, JSON, HTML, YAML, or anything else. The `.fluid` extension is just a convention; the template itself is plain text with Liquid tags. -### Including generated C# in compilation +### Compilation -By default, generated `.cs` files are not automatically compiled — MSBuild's SDK glob evaluates before templates are rendered. Add `Compile="true"` to include the output in compilation: +Generated `.cs` files are automatically included in compilation. To opt out, set `Compile="false"`: ```xml - + + +``` + +For non-`.cs` outputs that should be compiled, set `Compile="true"` explicitly: + +```xml + + ``` diff --git a/src/Fluidify/Fluidify.csproj b/src/Fluidify/Fluidify.csproj index d295c46..31ded5b 100644 --- a/src/Fluidify/Fluidify.csproj +++ b/src/Fluidify/Fluidify.csproj @@ -4,7 +4,7 @@ netstandard2.0 latest Fluidify - 0.1.0 + 0.0.0-local stanoddly A modern alternative to T4 — an MSBuild task that processes Fluid (Liquid) template files at build time to generate code, configuration, or any other text output. Apache-2.0 diff --git a/src/Fluidify/FluidifyTask.cs b/src/Fluidify/FluidifyTask.cs index 005efef..600cdaf 100644 --- a/src/Fluidify/FluidifyTask.cs +++ b/src/Fluidify/FluidifyTask.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using System.IO; using Fluid; using Microsoft.Build.Framework; @@ -9,6 +10,9 @@ namespace Fluidify; public class FluidifyTask : Task { + private static readonly HashSet ExcludedMetadataKeys = + new HashSet(StringComparer.OrdinalIgnoreCase) { "Destination", "Compile" }; + [Required] public ITaskItem[] Templates { get; set; } = Array.Empty(); @@ -47,8 +51,7 @@ public override bool Execute() foreach (DictionaryEntry entry in item.CloneCustomMetadata()) { string key = entry.Key.ToString(); - if (!string.Equals(key, "Destination", StringComparison.OrdinalIgnoreCase) && - !string.Equals(key, "Compile", StringComparison.OrdinalIgnoreCase)) + if (!ExcludedMetadataKeys.Contains(key)) { context.SetValue(key, entry.Value?.ToString() ?? ""); } diff --git a/src/Fluidify/build/Fluidify.targets b/src/Fluidify/build/Fluidify.targets index b193e43..50c837a 100644 --- a/src/Fluidify/build/Fluidify.targets +++ b/src/Fluidify/build/Fluidify.targets @@ -30,15 +30,17 @@ DependsOnTargets="ProcessFluidTemplates" Condition="'@(Fluidify)' != ''"> - + <_FluidifyCompileOutput Include="%(Fluidify.OutputPath)" + Condition="('%(Fluidify.Compile)' == 'true') or + ($([System.IO.Path]::GetExtension('%(Fluidify.OutputPath)')) == '.cs' and '%(Fluidify.Compile)' != 'false')" /> + + + - - + + + <_FluidifyCompileOutput Remove="@(_FluidifyCompileOutput)" /> diff --git a/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/CompileApp.csproj b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/CompileApp.csproj index 1164d7d..67d821b 100644 --- a/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/CompileApp.csproj +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/CompileApp.csproj @@ -7,7 +7,8 @@ - + + diff --git a/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/CompileFalseApp.csproj b/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/CompileFalseApp.csproj index d32b896..e678ca8 100644 --- a/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/CompileFalseApp.csproj +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/CompileFalseApp.csproj @@ -7,7 +7,8 @@ - + + diff --git a/tests/Fluidify.Tests.Functional/Fixtures/SampleApp/SampleApp.csproj b/tests/Fluidify.Tests.Functional/Fixtures/SampleApp/SampleApp.csproj index 44c91ca..4a2f50c 100644 --- a/tests/Fluidify.Tests.Functional/Fixtures/SampleApp/SampleApp.csproj +++ b/tests/Fluidify.Tests.Functional/Fixtures/SampleApp/SampleApp.csproj @@ -8,7 +8,8 @@ - + +