diff --git a/README.md b/README.md index 2f4a941..65c7374 100644 --- a/README.md +++ b/README.md @@ -16,25 +16,43 @@ 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. +### 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 + + + +``` + ### Custom output path Use the `Destination` metadata to write the output to a different location: @@ -55,7 +73,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/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 f26e826..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,7 +51,7 @@ public override bool Execute() foreach (DictionaryEntry entry in item.CloneCustomMetadata()) { string key = entry.Key.ToString(); - if (!string.Equals(key, "Destination", 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 9e0408e..50c837a 100644 --- a/src/Fluidify/build/Fluidify.targets +++ b/src/Fluidify/build/Fluidify.targets @@ -26,4 +26,22 @@ + + + + <_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/CompileTests.cs b/tests/Fluidify.Tests.Functional/CompileTests.cs new file mode 100644 index 0000000..92e2839 --- /dev/null +++ b/tests/Fluidify.Tests.Functional/CompileTests.cs @@ -0,0 +1,91 @@ +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}"); + } + + private static readonly string CompileFalseAppDir = Path.Combine( + SolutionRoot, "tests", "Fluidify.Tests.Functional", "Fixtures", "CompileFalseApp"); + + [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}"); + } + + [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) + { + 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..67d821b --- /dev/null +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileApp/CompileApp.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + Exe + enable + + + + + + + + + + + + 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 @@ + + + + + + + + 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..e678ca8 --- /dev/null +++ b/tests/Fluidify.Tests.Functional/Fixtures/CompileFalseApp/CompileFalseApp.csproj @@ -0,0 +1,18 @@ + + + + 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 @@ + + + + + + + + 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 @@ - + +