mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 01:18:22 +00:00
175 lines
5.6 KiB
C#
175 lines
5.6 KiB
C#
// Copyright (c) Duende Software. All rights reserved.
|
|
// See LICENSE in the project root for license information.
|
|
|
|
using Duende.ValueObjectsGenerator;
|
|
|
|
// Simple argument parsing — this is internal tooling, no framework needed
|
|
string? path = null;
|
|
string? ns = null;
|
|
|
|
for (var i = 0; i < args.Length; i++)
|
|
{
|
|
if (args[i] == "--path" && i + 1 < args.Length)
|
|
{
|
|
path = args[++i];
|
|
}
|
|
else if (args[i] == "--namespace" && i + 1 < args.Length)
|
|
{
|
|
ns = args[++i];
|
|
}
|
|
}
|
|
|
|
if (path is null || ns is null)
|
|
{
|
|
Console.Error.WriteLine("Usage: ValueObjectsGenerator --path <projectDir> --namespace <rootNamespace>");
|
|
return 1;
|
|
}
|
|
|
|
if (!Directory.Exists(path))
|
|
{
|
|
Console.Error.WriteLine($"Directory not found: {path}");
|
|
return 1;
|
|
}
|
|
|
|
try
|
|
{
|
|
return Run(path, ns);
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException or ArgumentException)
|
|
{
|
|
Console.Error.WriteLine($"ValueObjectsGenerator failed: {ex.Message}");
|
|
return 1;
|
|
}
|
|
|
|
static int Run(string path, string ns)
|
|
{
|
|
var valueObjects = FileScanner.Scan(path, ns);
|
|
|
|
var written = 0;
|
|
var skipped = 0;
|
|
var expectedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
// Generate per-type .g.cs files next to their source files
|
|
foreach (var vo in valueObjects)
|
|
{
|
|
var sourceDir = Path.GetDirectoryName(vo.SourceFilePath)!;
|
|
var outputPath = Path.Combine(sourceDir, $"{vo.TypeName}.g.cs");
|
|
|
|
var content = vo.Kind switch
|
|
{
|
|
ValueObjectKind.StringValue => StringValueCodeGenerator.Generate(vo),
|
|
ValueObjectKind.ValueOf => ValueOfCodeGenerator.Generate(vo),
|
|
_ => throw new InvalidOperationException($"Unknown kind: {vo.Kind}")
|
|
};
|
|
|
|
_ = expectedFiles.Add(Path.GetFullPath(outputPath));
|
|
|
|
if (WriteIfChanged(outputPath, content))
|
|
{
|
|
written++;
|
|
}
|
|
else
|
|
{
|
|
skipped++;
|
|
}
|
|
}
|
|
|
|
// Generate infrastructure files in {path}/Internal/ValueObjects/
|
|
var valueObjectsDir = Path.Combine(path, "Internal", "ValueObjects");
|
|
_ = Directory.CreateDirectory(valueObjectsDir);
|
|
|
|
TrackAndWrite(ref written, ref skipped, expectedFiles,
|
|
Path.Combine(valueObjectsDir, "IValueOf.g.cs"), InfrastructureCodeGenerator.GetInterfaceSource(ns));
|
|
|
|
TrackAndWrite(ref written, ref skipped, expectedFiles,
|
|
Path.Combine(valueObjectsDir, "ValueOfTypeConverter.g.cs"), InfrastructureCodeGenerator.GetTypeConverterSource(ns));
|
|
|
|
TrackAndWrite(ref written, ref skipped, expectedFiles,
|
|
Path.Combine(valueObjectsDir, "CharsetExtensions.g.cs"), InfrastructureCodeGenerator.GetCharSetsSource(ns));
|
|
|
|
// Clean up orphaned .g.cs files that were previously generated by this tool
|
|
var deleted = CleanOrphanedFiles(path, expectedFiles);
|
|
|
|
Console.WriteLine($"ValueObjectsGenerator: {valueObjects.Count} value objects — {written} files written, {skipped} unchanged, {deleted} orphans deleted.");
|
|
return 0;
|
|
}
|
|
|
|
static void TrackAndWrite(ref int written, ref int skipped, HashSet<string> expectedFiles, string filePath, string content)
|
|
{
|
|
_ = expectedFiles.Add(Path.GetFullPath(filePath));
|
|
if (WriteIfChanged(filePath, content))
|
|
{
|
|
written++;
|
|
}
|
|
else
|
|
{
|
|
skipped++;
|
|
}
|
|
}
|
|
|
|
// Deletes orphaned .g.cs files that were previously generated by this tool but are no longer expected.
|
|
// Only deletes files that contain the ValueObjectsGenerator marker comment.
|
|
static int CleanOrphanedFiles(string rootPath, HashSet<string> expectedFiles)
|
|
{
|
|
const string marker = "// <auto-generated by=\"ValueObjectsGenerator\"/>";
|
|
var deleted = 0;
|
|
|
|
foreach (var file in Directory.EnumerateFiles(rootPath, "*.g.cs", SearchOption.AllDirectories))
|
|
{
|
|
var fullPath = Path.GetFullPath(file);
|
|
|
|
// Skip files in obj/ and bin/ directories
|
|
var normalized = fullPath.Replace('\\', '/');
|
|
if (normalized.Contains("/obj/", StringComparison.Ordinal) ||
|
|
normalized.Contains("/bin/", StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Skip files we just generated
|
|
if (expectedFiles.Contains(fullPath))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Only delete files that were generated by this tool (contain our marker)
|
|
string? firstLine;
|
|
using (var reader = new StreamReader(file))
|
|
{
|
|
firstLine = reader.ReadLine();
|
|
}
|
|
|
|
if (firstLine == marker)
|
|
{
|
|
File.Delete(file);
|
|
Console.WriteLine($" Deleted orphaned file: {file}");
|
|
deleted++;
|
|
}
|
|
}
|
|
|
|
return deleted;
|
|
}
|
|
|
|
// Writes content to path only if it differs from what's already there.
|
|
// Returns true if the file was written, false if unchanged or locked.
|
|
static bool WriteIfChanged(string filePath, string content)
|
|
{
|
|
if (File.Exists(filePath))
|
|
{
|
|
var existing = File.ReadAllText(filePath);
|
|
// Normalize line endings for comparison
|
|
if (existing.Replace("\r\n", "\n", StringComparison.Ordinal) == content.Replace("\r\n", "\n", StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Use FileShare.ReadWrite so IDE language servers holding a shared lock don't block us.
|
|
// FileMode.OpenOrCreate + SetLength(0) avoids the truncation that FileMode.Create requires
|
|
// (which needs exclusive access on Windows).
|
|
using var stream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite);
|
|
stream.SetLength(0);
|
|
using var writer = new StreamWriter(stream, System.Text.Encoding.UTF8);
|
|
writer.Write(content);
|
|
return true;
|
|
}
|