diff --git a/src/TALXIS.CLI.Workspace/TALXIS.CLI.Workspace.csproj b/src/TALXIS.CLI.Workspace/TALXIS.CLI.Workspace.csproj index e916c87..7ad583f 100644 --- a/src/TALXIS.CLI.Workspace/TALXIS.CLI.Workspace.csproj +++ b/src/TALXIS.CLI.Workspace/TALXIS.CLI.Workspace.csproj @@ -15,4 +15,12 @@ + + + PreserveNewest + PreserveNewest + Upgrade/Templates/%(RecursiveDir)%(Filename)%(Extension) + + + diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Conversion/ProjectUpgrader.cs b/src/TALXIS.CLI.Workspace/Upgrade/Conversion/ProjectUpgrader.cs new file mode 100644 index 0000000..6cf8476 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Conversion/ProjectUpgrader.cs @@ -0,0 +1,196 @@ +using Microsoft.Extensions.Logging; +using TALXIS.CLI.Workspace.Upgrade.Generation; +using TALXIS.CLI.Workspace.Upgrade.Models; +using TALXIS.CLI.Workspace.Upgrade.Parsers; +using TALXIS.CLI.Workspace.Upgrade.Utilities; + +namespace TALXIS.CLI.Workspace.Upgrade.Conversion; + +public class ProjectUpgrader +{ + private readonly CsprojParser _parser; + private readonly SdkProjectGenerator _generator; + private readonly ILogger _logger; + + public ProjectUpgrader(ILogger logger) + { + _parser = new CsprojParser(); + _generator = new SdkProjectGenerator(); + _logger = logger; + } + + public UpgradeResult Upgrade( + string targetFilePath, + string oldTemplateFilePath, + string newTemplateFilePath, + bool createBackup = true) + { + var result = new UpgradeResult(); + + try + { + _logger.LogInformation("Parsing project and templates..."); + var targetProject = _parser.Parse(targetFilePath); + var oldTemplate = _parser.Parse(oldTemplateFilePath); + var newTemplate = _parser.Parse(newTemplateFilePath); + + _logger.LogInformation("Filling parameter placeholders from existing project..."); + var newProject = FillParameterPlaceholders(newTemplate, targetProject, targetFilePath); + + _logger.LogInformation("Extracting additional dependencies not in baseline template..."); + ExtractAdditionalDependencies(oldTemplate, targetProject, newProject); + + result.CustomPropertiesFound = CountFilledParameters(newTemplate, targetProject); + result.PackageReferencesFound = newProject.PackageReferences.Count - newTemplate.PackageReferences.Count; + result.ProjectReferencesFound = newProject.ProjectReferences.Count - newTemplate.ProjectReferences.Count; + result.AssemblyReferencesFound = newProject.AssemblyReferences.Count - newTemplate.AssemblyReferences.Count; + + if (createBackup) + { + var backupPath = targetFilePath + ".backup"; + if (File.Exists(targetFilePath)) + { + File.Copy(targetFilePath, backupPath, overwrite: true); + result.BackupCreated = true; + result.BackupPath = backupPath; + _logger.LogInformation("Backup created at {BackupPath}", backupPath); + } + } + + if (File.Exists(targetFilePath)) + { + _logger.LogInformation("Removing original file {TargetFile}", targetFilePath); + File.Delete(targetFilePath); + } + + var newFilePath = Path.Combine( + Path.GetDirectoryName(targetFilePath) ?? string.Empty, + Path.GetFileNameWithoutExtension(targetFilePath) + ".csproj"); + + _logger.LogInformation("Generating new SDK-style project file at {NewFilePath}", newFilePath); + var xml = _generator.Generate(newProject); + _generator.SaveToFile(xml, newFilePath); + + result.Success = true; + result.OutputFilePath = newFilePath; + } + catch (Exception ex) + { + result.Success = false; + result.ErrorMessage = ex.Message; + result.Exception = ex; + _logger.LogError(ex, "Upgrade failed for {TargetFile}", targetFilePath); + } + + return result; + } + + private CsprojProject FillParameterPlaceholders( + CsprojProject newTemplate, + CsprojProject targetProject, + string targetFilePath) + { + var newProject = new CsprojProject + { + FilePath = targetFilePath.Replace(".cdsproj", ".csproj"), + IsSdkStyle = true, + Sdk = newTemplate.Sdk ?? "Microsoft.NET.Sdk" + }; + + // Copy all properties + foreach (var prop in newTemplate.Properties) + { + // If template property has empty value, it's a parameter placeholder - fill from target + if (string.IsNullOrWhiteSpace(prop.Value)) + { + if (targetProject.Properties.TryGetValue(prop.Key, out var targetValue)) + { + newProject.Properties[prop.Key] = targetValue; + } + else + { + newProject.Properties[prop.Key] = prop.Value; + } + } + else + { + newProject.Properties[prop.Key] = prop.Value; + } + } + + // Copy all other elements + newProject.PackageReferences.AddRange(newTemplate.PackageReferences); + newProject.ProjectReferences.AddRange(newTemplate.ProjectReferences); + newProject.AssemblyReferences.AddRange(newTemplate.AssemblyReferences); + newProject.CustomTargets.AddRange(newTemplate.CustomTargets); + newProject.CustomImports.AddRange(newTemplate.CustomImports); + newProject.CustomItemGroups.AddRange(newTemplate.CustomItemGroups); + newProject.CustomPropertyGroups.AddRange(newTemplate.CustomPropertyGroups); + + return newProject; + } + + private int CountFilledParameters(CsprojProject newTemplate, CsprojProject targetProject) + { + int count = 0; + foreach (var prop in newTemplate.Properties) + { + if (string.IsNullOrWhiteSpace(prop.Value) && targetProject.Properties.ContainsKey(prop.Key)) + { + count++; + } + } + return count; + } + + private void ExtractAdditionalDependencies( + CsprojProject oldTemplate, + CsprojProject targetProject, + CsprojProject newProject) + { + // Find PackageReferences that are in target but NOT in old template baseline + var baselinePackages = new HashSet( + oldTemplate.PackageReferences.Select(p => p.Name), + StringComparer.OrdinalIgnoreCase + ); + + foreach (var package in targetProject.PackageReferences) + { + if (!baselinePackages.Contains(package.Name)) + { + // This is a custom package added by user + newProject.PackageReferences.Add(package); + } + } + + // Find ProjectReferences that are in target but NOT in old template baseline + var baselineProjects = new HashSet( + oldTemplate.ProjectReferences.Select(p => p.Include), + StringComparer.OrdinalIgnoreCase + ); + + foreach (var projectRef in targetProject.ProjectReferences) + { + if (!baselineProjects.Contains(projectRef.Include)) + { + // This is a custom project reference added by user + newProject.ProjectReferences.Add(projectRef); + } + } + + // Find AssemblyReferences that are in target but NOT in old template baseline + var baselineAssemblies = new HashSet( + oldTemplate.AssemblyReferences.Select(a => a.Include), + StringComparer.OrdinalIgnoreCase + ); + + foreach (var assembly in targetProject.AssemblyReferences) + { + if (!baselineAssemblies.Contains(assembly.Include)) + { + // This is a custom assembly reference added by user + newProject.AssemblyReferences.Add(assembly); + } + } + } +} diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Generation/SdkProjectGenerator.cs b/src/TALXIS.CLI.Workspace/Upgrade/Generation/SdkProjectGenerator.cs new file mode 100644 index 0000000..2572a10 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Generation/SdkProjectGenerator.cs @@ -0,0 +1,184 @@ +using System.Xml.Linq; +using TALXIS.CLI.Workspace.Upgrade.Models; + +namespace TALXIS.CLI.Workspace.Upgrade.Generation; + +public class SdkProjectGenerator +{ + public XDocument Generate(CsprojProject project) + { + var root = new XElement("Project", + new XAttribute("Sdk", project.Sdk ?? "Microsoft.NET.Sdk") + ); + + AddPropertyGroup(root, project); + + if (project.PackageReferences.Any()) + { + AddPackageReferences(root, project); + } + + if (project.ProjectReferences.Any()) + { + AddProjectReferences(root, project); + } + + if (project.AssemblyReferences.Any()) + { + AddAssemblyReferences(root, project); + } + + foreach (var itemGroup in project.CustomItemGroups) + { + root.Add(itemGroup); + } + + foreach (var import in project.CustomImports) + { + root.Add(import); + } + + foreach (var target in project.CustomTargets) + { + root.Add(target); + } + + return new XDocument( + new XDeclaration("1.0", "utf-8", null), + root + ); + } + + + public void SaveToFile(XDocument document, string filePath) + { + var directory = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + document.Save(filePath, SaveOptions.None); + } + + private void AddPropertyGroup(XElement root, CsprojProject project) + { + if (!project.Properties.Any()) + return; + + var propertyGroup = new XElement("PropertyGroup"); + + var orderedProperties = new[] + { + "TargetFramework", "TargetFrameworks", + "OutputType", "AssemblyName", "RootNamespace", + "LangVersion", "Nullable", "ImplicitUsings" + }; + + foreach (var key in orderedProperties) + { + if (project.Properties.TryGetValue(key, out var value)) + { + propertyGroup.Add(new XElement(key, value)); + } + } + + foreach (var prop in project.Properties) + { + if (!orderedProperties.Contains(prop.Key)) + { + propertyGroup.Add(new XElement(prop.Key, prop.Value)); + } + } + + root.Add(propertyGroup); + } + + private void AddPackageReferences(XElement root, CsprojProject project) + { + var itemGroup = new XElement("ItemGroup"); + + var attributeNames = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "PrivateAssets", "ExcludeAssets", "IncludeAssets" + }; + + foreach (var package in project.PackageReferences.OrderBy(p => p.Name)) + { + var element = new XElement("PackageReference", + new XAttribute("Include", package.Name) + ); + + if (!string.IsNullOrEmpty(package.Version)) + { + element.Add(new XAttribute("Version", package.Version)); + } + + // Add metadata + foreach (var metadata in package.Metadata) + { + if (attributeNames.Contains(metadata.Key)) + { + element.Add(new XAttribute(metadata.Key, metadata.Value)); + } + else + { + element.Add(new XElement(metadata.Key, metadata.Value)); + } + } + + itemGroup.Add(element); + } + + root.Add(itemGroup); + } + + private void AddProjectReferences(XElement root, CsprojProject project) + { + var itemGroup = new XElement("ItemGroup"); + + foreach (var projectRef in project.ProjectReferences.OrderBy(p => p.Include)) + { + var element = new XElement("ProjectReference", + new XAttribute("Include", projectRef.Include) + ); + + // Add metadata + foreach (var metadata in projectRef.Metadata) + { + element.Add(new XElement(metadata.Key, metadata.Value)); + } + + itemGroup.Add(element); + } + + root.Add(itemGroup); + } + + private void AddAssemblyReferences(XElement root, CsprojProject project) + { + var itemGroup = new XElement("ItemGroup"); + + foreach (var assemblyRef in project.AssemblyReferences.OrderBy(a => a.Include)) + { + var element = new XElement("Reference", + new XAttribute("Include", assemblyRef.Include) + ); + + if (!string.IsNullOrEmpty(assemblyRef.HintPath)) + { + element.Add(new XElement("HintPath", assemblyRef.HintPath)); + } + + // Add metadata + foreach (var metadata in assemblyRef.Metadata) + { + element.Add(new XElement(metadata.Key, metadata.Value)); + } + + itemGroup.Add(element); + } + + root.Add(itemGroup); + } +} diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Models/AssemblyReference.cs b/src/TALXIS.CLI.Workspace/Upgrade/Models/AssemblyReference.cs new file mode 100644 index 0000000..c055d94 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Models/AssemblyReference.cs @@ -0,0 +1,10 @@ +namespace TALXIS.CLI.Workspace.Upgrade.Models; + +public class AssemblyReference +{ + public string Include { get; set; } = string.Empty; + public string? HintPath { get; set; } + public Dictionary Metadata { get; set; } = new(); + + public override string ToString() => Include; +} diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Models/CsprojProject.cs b/src/TALXIS.CLI.Workspace/Upgrade/Models/CsprojProject.cs new file mode 100644 index 0000000..71343b9 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Models/CsprojProject.cs @@ -0,0 +1,27 @@ +using System.Xml.Linq; + +namespace TALXIS.CLI.Workspace.Upgrade.Models; + +public class CsprojProject +{ + public string FilePath { get; set; } = string.Empty; + public bool IsSdkStyle { get; set; } + public string? Sdk { get; set; } + + // Properties + public Dictionary Properties { get; set; } = new(); + + // References + public List PackageReferences { get; set; } = new(); + public List ProjectReferences { get; set; } = new(); + public List AssemblyReferences { get; set; } = new(); + + // Custom elements that need to be preserved + public List CustomPropertyGroups { get; set; } = new(); + public List CustomItemGroups { get; set; } = new(); + public List CustomTargets { get; set; } = new(); + public List CustomImports { get; set; } = new(); + + // Original XML for reference + public XDocument? OriginalXml { get; set; } +} diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Models/PackageReference.cs b/src/TALXIS.CLI.Workspace/Upgrade/Models/PackageReference.cs new file mode 100644 index 0000000..67a3ad5 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Models/PackageReference.cs @@ -0,0 +1,10 @@ +namespace TALXIS.CLI.Workspace.Upgrade.Models; + +public class PackageReference +{ + public string Name { get; set; } = string.Empty; + public string? Version { get; set; } + public Dictionary Metadata { get; set; } = new(); + + public override string ToString() => $"{Name} {Version}"; +} diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Models/ProjectReference.cs b/src/TALXIS.CLI.Workspace/Upgrade/Models/ProjectReference.cs new file mode 100644 index 0000000..4c880bf --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Models/ProjectReference.cs @@ -0,0 +1,9 @@ +namespace TALXIS.CLI.Workspace.Upgrade.Models; + +public class ProjectReference +{ + public string Include { get; set; } = string.Empty; + public Dictionary Metadata { get; set; } = new(); + + public override string ToString() => Include; +} diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Models/ProjectType.cs b/src/TALXIS.CLI.Workspace/Upgrade/Models/ProjectType.cs new file mode 100644 index 0000000..11f4df3 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Models/ProjectType.cs @@ -0,0 +1,10 @@ +namespace TALXIS.CLI.Workspace.Upgrade.Models; + +public enum ProjectType +{ + DataverseSolution, + ScriptLibrary, + Plugin, + PDPackage, + Unknown +} diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Models/UpgradeResult.cs b/src/TALXIS.CLI.Workspace/Upgrade/Models/UpgradeResult.cs new file mode 100644 index 0000000..1f33aff --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Models/UpgradeResult.cs @@ -0,0 +1,15 @@ +namespace TALXIS.CLI.Workspace.Upgrade.Models; + +public class UpgradeResult +{ + public bool Success { get; set; } + public string? OutputFilePath { get; set; } + public string? BackupPath { get; set; } + public bool BackupCreated { get; set; } + public int PackageReferencesFound { get; set; } + public int ProjectReferencesFound { get; set; } + public int AssemblyReferencesFound { get; set; } + public int CustomPropertiesFound { get; set; } + public string? ErrorMessage { get; set; } + public Exception? Exception { get; set; } +} diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Parsers/CsprojParser.cs b/src/TALXIS.CLI.Workspace/Upgrade/Parsers/CsprojParser.cs new file mode 100644 index 0000000..e66a018 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Parsers/CsprojParser.cs @@ -0,0 +1,195 @@ +using System.Xml.Linq; +using TALXIS.CLI.Workspace.Upgrade.Models; + +namespace TALXIS.CLI.Workspace.Upgrade.Parsers; + +public class CsprojParser +{ + private static readonly XNamespace MsbuildNamespace = "http://schemas.microsoft.com/developer/msbuild/2003"; + + public CsprojProject Parse(string filePath) + { + if (!File.Exists(filePath)) + throw new FileNotFoundException($"Project file not found: {filePath}"); + + var xml = XDocument.Load(filePath); + var root = xml.Root ?? throw new InvalidOperationException("Invalid project file: no root element"); + + var project = new CsprojProject + { + FilePath = filePath, + OriginalXml = xml, + IsSdkStyle = root.Attribute("Sdk") != null, + Sdk = root.Attribute("Sdk")?.Value + }; + + ParseProperties(root, project); + ParsePackageReferences(root, project); + ParseProjectReferences(root, project); + ParseAssemblyReferences(root, project); + ParseCustomElements(root, project); + + return project; + } + + private void ParseProperties(XElement root, CsprojProject project) + { + // Parse all PropertyGroup elements + var propertyGroups = root.Elements(MsbuildNamespace + "PropertyGroup") + .Concat(root.Elements("PropertyGroup")); + + foreach (var group in propertyGroups) + { + foreach (var property in group.Elements()) + { + var name = property.Name.LocalName; + var value = property.Value; + + // Store all properties + if (!project.Properties.ContainsKey(name)) + { + project.Properties[name] = value; + } + } + } + } + + private void ParsePackageReferences(XElement root, CsprojProject project) + { + var packageRefs = root.Descendants(MsbuildNamespace + "PackageReference") + .Concat(root.Descendants("PackageReference")); + + foreach (var packageRef in packageRefs) + { + var include = packageRef.Attribute("Include")?.Value; + if (string.IsNullOrEmpty(include)) + continue; + + var package = new PackageReference + { + Name = include, + Version = packageRef.Attribute("Version")?.Value + ?? packageRef.Element(MsbuildNamespace + "Version")?.Value + ?? packageRef.Element("Version")?.Value + }; + + // Parse attributes as metadata (except Include and Version) + foreach (var attribute in packageRef.Attributes()) + { + if (attribute.Name.LocalName != "Include" && attribute.Name.LocalName != "Version") + { + package.Metadata[attribute.Name.LocalName] = attribute.Value; + } + } + + // Parse child elements as metadata + foreach (var element in packageRef.Elements()) + { + if (element.Name.LocalName != "Version") + { + package.Metadata[element.Name.LocalName] = element.Value; + } + } + + project.PackageReferences.Add(package); + } + } + + private void ParseProjectReferences(XElement root, CsprojProject project) + { + var projectRefs = root.Descendants(MsbuildNamespace + "ProjectReference") + .Concat(root.Descendants("ProjectReference")); + + foreach (var projectRef in projectRefs) + { + var include = projectRef.Attribute("Include")?.Value; + if (string.IsNullOrEmpty(include)) + continue; + + var reference = new ProjectReference + { + Include = include + }; + + // Parse metadata + foreach (var element in projectRef.Elements()) + { + reference.Metadata[element.Name.LocalName] = element.Value; + } + + project.ProjectReferences.Add(reference); + } + } + + private void ParseAssemblyReferences(XElement root, CsprojProject project) + { + var assemblyRefs = root.Descendants(MsbuildNamespace + "Reference") + .Concat(root.Descendants("Reference")); + + foreach (var assemblyRef in assemblyRefs) + { + var include = assemblyRef.Attribute("Include")?.Value; + if (string.IsNullOrEmpty(include)) + continue; + + var reference = new AssemblyReference + { + Include = include, + HintPath = assemblyRef.Element(MsbuildNamespace + "HintPath")?.Value + ?? assemblyRef.Element("HintPath")?.Value + }; + + // Parse metadata + foreach (var element in assemblyRef.Elements()) + { + if (element.Name.LocalName != "HintPath") + { + reference.Metadata[element.Name.LocalName] = element.Value; + } + } + + project.AssemblyReferences.Add(reference); + } + } + + private void ParseCustomElements(XElement root, CsprojProject project) + { + // Collect custom Target elements + var targets = root.Descendants(MsbuildNamespace + "Target") + .Concat(root.Descendants("Target")); + project.CustomTargets.AddRange(targets); + + // Collect custom Import elements + var imports = root.Elements(MsbuildNamespace + "Import") + .Concat(root.Elements("Import")); + project.CustomImports.AddRange(imports); + + // Store ItemGroups only if they contain elements OTHER than PackageReference, ProjectReference, or Reference + // Those are already parsed separately and will be added by the generator + var itemGroups = root.Elements(MsbuildNamespace + "ItemGroup") + .Concat(root.Elements("ItemGroup")); + + foreach (var itemGroup in itemGroups) + { + var hasStandardReferences = itemGroup.Elements() + .Any(e => e.Name.LocalName == "PackageReference" || + e.Name.LocalName == "ProjectReference" || + e.Name.LocalName == "Reference"); + + var hasOtherElements = itemGroup.Elements() + .Any(e => e.Name.LocalName != "PackageReference" && + e.Name.LocalName != "ProjectReference" && + e.Name.LocalName != "Reference"); + + + if (hasOtherElements && !hasStandardReferences) + { + project.CustomItemGroups.Add(itemGroup); + } + } + + var propertyGroups = root.Elements(MsbuildNamespace + "PropertyGroup") + .Concat(root.Elements("PropertyGroup")); + project.CustomPropertyGroups.AddRange(propertyGroups); + } +} diff --git a/src/TALXIS.CLI.Workspace/Upgrade/ProjectUpgradeRunner.cs b/src/TALXIS.CLI.Workspace/Upgrade/ProjectUpgradeRunner.cs new file mode 100644 index 0000000..0be66b9 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/ProjectUpgradeRunner.cs @@ -0,0 +1,145 @@ +using Microsoft.Extensions.Logging; +using TALXIS.CLI.Workspace.Upgrade.Conversion; +using TALXIS.CLI.Workspace.Upgrade.Models; +using TALXIS.CLI.Workspace.Upgrade.Utilities; + +namespace TALXIS.CLI.Workspace.Upgrade; + +/// +/// Coordinates upgrading one or more project files. +/// +public class ProjectUpgradeRunner +{ + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly string _templatesBasePath; + private readonly bool _createBackup; + + public ProjectUpgradeRunner( + ILoggerFactory loggerFactory, + string templatesBasePath, + bool createBackup = true) + { + _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + _logger = loggerFactory.CreateLogger(); + _templatesBasePath = templatesBasePath ?? throw new ArgumentNullException(nameof(templatesBasePath)); + _createBackup = createBackup; + } + + public int Run(string targetPath) + { + if (string.IsNullOrWhiteSpace(targetPath)) + { + _logger.LogError("Path to project file or directory must be provided."); + return 1; + } + + targetPath = Path.GetFullPath(targetPath); + + var projectFiles = ResolveTargets(targetPath); + if (projectFiles.Count == 0) + { + _logger.LogError("No .csproj or .cdsproj files found at {TargetPath}", targetPath); + return 1; + } + + _logger.LogInformation("Found {Count} project file(s) to upgrade.", projectFiles.Count); + + var detector = new ProjectTypeDetector(); + var templateManager = new TemplateManager(_templatesBasePath); + + int processed = 0, succeeded = 0, failed = 0; + + foreach (var projectFile in projectFiles) + { + processed++; + _logger.LogInformation("[{Index}/{Total}] Processing {File}", processed, projectFiles.Count, projectFile); + + var projectType = detector.DetectProjectType(projectFile); + if (projectType == ProjectType.Unknown) + { + _logger.LogError("Unsupported project type for {File}. Supported: Dataverse Solution, Script Library, Plugin, PDPackage.", projectFile); + failed++; + continue; + } + + var isOldTalxisFormat = projectType == ProjectType.DataverseSolution && detector.IsOldTalxisFormat(projectFile); + if (isOldTalxisFormat) + { + _logger.LogInformation("Detected old TALXIS Dataverse Solution format for {File}", projectFile); + } + + string oldTemplatePath; + string newTemplatePath; + try + { + oldTemplatePath = templateManager.GetOldFormatTemplatePath(projectType, isOldTalxisFormat); + newTemplatePath = templateManager.GetNewFormatTemplatePath(projectType); + templateManager.ValidateTemplates(projectType, isOldTalxisFormat); + } + catch (FileNotFoundException ex) + { + _logger.LogError(ex, "Template files missing for {ProjectType}", projectType); + failed++; + continue; + } + + _logger.LogInformation("Using templates. Old: {OldTemplate} | New: {NewTemplate}", Path.GetFileName(oldTemplatePath), Path.GetFileName(newTemplatePath)); + + var upgraderLogger = _loggerFactory.CreateLogger(); + var upgrader = new ProjectUpgrader(upgraderLogger); + var result = upgrader.Upgrade(projectFile, oldTemplatePath, newTemplatePath, _createBackup); + + if (result.Success) + { + _logger.LogInformation("Upgrade successful. Output: {Output}", result.OutputFilePath); + if (result.BackupCreated) + { + _logger.LogInformation("Backup saved at {Backup}", result.BackupPath); + } + _logger.LogInformation("Transferred: {Packages} package refs, {Projects} project refs, {Assemblies} assembly refs, {Props} custom properties.", + result.PackageReferencesFound, result.ProjectReferencesFound, result.AssemblyReferencesFound, result.CustomPropertiesFound); + succeeded++; + } + else + { + _logger.LogError("Upgrade failed for {File}: {Message}", projectFile, result.ErrorMessage); + if (result.Exception != null) + { + _logger.LogDebug(result.Exception, "Details for {File}", projectFile); + } + if (result.BackupCreated && !string.IsNullOrWhiteSpace(result.BackupPath)) + { + _logger.LogWarning("Original file backed up at {Backup}", result.BackupPath); + } + failed++; + } + } + + if (projectFiles.Count > 1) + { + _logger.LogInformation("Summary: processed {Processed}, successful {Succeeded}, failed {Failed}", processed, succeeded, failed); + } + + return failed == 0 ? 0 : 1; + } + + private List ResolveTargets(string targetPath) + { + var projectFiles = new List(); + + if (File.Exists(targetPath)) + { + projectFiles.Add(targetPath); + return projectFiles; + } + + if (Directory.Exists(targetPath)) + { + projectFiles.AddRange(Directory.GetFiles(targetPath, "*.csproj", SearchOption.AllDirectories)); + projectFiles.AddRange(Directory.GetFiles(targetPath, "*.cdsproj", SearchOption.AllDirectories)); + } + + return projectFiles; + } +} diff --git a/src/TALXIS.CLI.Workspace/Upgrade/TALXIS-Plugin-Migration-README.md b/src/TALXIS.CLI.Workspace/Upgrade/TALXIS-Plugin-Migration-README.md new file mode 100644 index 0000000..8478820 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/TALXIS-Plugin-Migration-README.md @@ -0,0 +1,144 @@ +# TALXIS: Migrating Power Platform (Dataverse) plugins to the new project format + +This README describes the **standard TALXIS process** for migrating plugins from the old project format to the new one. + +TALXIS **templates** and TALXIS **build targets** automatically take care of building/packaging. Migration basically comes down to: +- creating a new plugin project using a TALXIS template +- moving the source code +- referencing the plugin project from the solution project via `ProjectReference` +- bringing the `PluginAssemblies` folder structure to the standard + +--- + +## Goal + +Split the old structure (where plugin code could live in the same project/folder tree as the solution) into two projects: + +- **Plugin project** (plugins) — created using a **TALXIS `dotnet new` template** +- **Solution project** (Dataverse solution) — built using **TALXIS build targets** + +After that, the projects are linked with a standard `ProjectReference`. + +--- + +## Requirements + +- TALXIS.DevKit.SDK is installed +- TALXIS templates (`dotnet new`) are installed +- Plugin source code is available in the old structure +- Power Platform Solution must already be upgraded to the TALXIS.DevKit.SDK format (the upgrade is performed using TALXIS.DevKit.Dataverse.Project.Upgrade.Tool) + +--- + +## Step-by-step migration + +### 1) Create a new Plugin project using a TALXIS template + +Create a new plugin project with `dotnet new`: + +```console +dotnet new pp-plugin ` +--output "src/Plugins.Project" ` +--PublisherName "tomas" ` +--SigningKeyFilePath "" ` +--Company "NETWORG" ` +--allow-scripts yes +``` + +**Result:** +- `Plugins.Project.csproj` is created + +--- + +### 2) Move the source code into the new Plugin project + +Move the source code from the old project into the new plugin project. + +**Move:** +- plugin classes (`*.cs`) +- helper/shared classes that were used within the same plugin assembly +- required resources used by plugins (only if they are actually needed): `*.resx`, `*.json`, `*.xml`, etc. + +**Result:** +- the new plugin project contains the full set of sources required to build the plugin assembly. + +--- + +### 3) Reference the Plugin project from the Solution project via `ProjectReference` + +Open the solution project `Solution.csproj` (the one built by TALXIS build targets) and add a `ProjectReference`: + +```xml + + + +``` + +**Result:** +- the solution project sees the plugin project as a dependency. + +--- + +### 4) Bring `PluginAssemblies` to the standard structure + +TALXIS expects the plugin assembly to be stored in a separate subfolder under `PluginAssemblies`. + +Required structure: + +```text +PluginAssemblies\-\ +``` + +Where: +- `AssemblyName` = the plugin name +- `AssemblyGuid` = the GUID id (in uppercase) of the plugin assembly +Example: + +```text +PluginAssemblies\Plugins.Project-571BCBFE-AAF4-4BE5-A8AE-424E878CBDEC\ +``` + +#### What you need to do + +1. Under `PluginAssemblies`, create a subfolder `AssemblyName-AssemblyGuid`. +2. If your plugin assembly files currently live directly under `PluginAssemblies\`, move them into the created subfolder. + +--- + +### 5) Plugin Assembly behavior (TALXIS) + +TALXIS build targets support both scenarios: + +1. **Generate Plugin Assembly automatically** (if the assembly does not exist yet). +2. **Update an existing Plugin Assembly** (if the assembly already exists). + +This means the folder: + +```text +PluginAssemblies\-\ +``` + +can be used in two ways: + +- **Empty folder**: TALXIS build targets will generate the required files/metadata during the build. +- **Folder with files**: you can pre-copy the assembly from a previous version (old repo/branch/release) into this folder — TALXIS build targets will take it as a base and update it to the current state. + +--- + +### 6) Verification + +1. Build the solution project using the standard `dotnet build` command. +2. Verify that: + - the solution project has a `ProjectReference` to the plugin project + - `PluginAssemblies` contains a subfolder `AssemblyName-AssemblyGuid` + - plugin assembly files are located inside that subfolder + - TALXIS build targets generate or update the assembly as expected + +--- + +## Summary + +- The **plugin project** is created using a TALXIS template → then plugin sources are moved into it. +- The **solution project** references the plugin project via `ProjectReference`. +- **PluginAssemblies** matches the standard: `PluginAssemblies\-\`. +- The assembly can be generated automatically or copied from a previous version and then updated by TALXIS build targets. diff --git a/src/TALXIS.CLI.Workspace/Upgrade/TALXIS-ScriptLibrary-Migration-README.md b/src/TALXIS.CLI.Workspace/Upgrade/TALXIS-ScriptLibrary-Migration-README.md new file mode 100644 index 0000000..1b1d118 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/TALXIS-ScriptLibrary-Migration-README.md @@ -0,0 +1,96 @@ +# TALXIS: Migrate Script Library to the New Project Format + +This README describes the **standard TALXIS migration flow** for moving a Script Library from an old structure to the new TALXIS project format. + +TALXIS **templates** and TALXIS **build targets** handle build behavior automatically. The migration is mainly: +- create a new ScriptLibrary project using the **pp-script-library** template +- move the `TS` source folder into the new project +- remove legacy configuration from old files +- update names so build artifacts do **not** contain `publisherPrefix_` (prefix is added during build) +- reference the ScriptLibrary project from the updated Solution project + +--- + +## Prerequisites + +- Power Platform Solution must already be upgraded to **TALXIS.DevKit.SDK** format + (the upgrade is performed using **TALXIS.DevKit.Dataverse.Project.Upgrade.Tool**). +- TALXIS .NET templates installed (`dotnet new`), including **pp-script-library** + +--- + +## Goal + +Split the old layout into two projects: + +- **ScriptLibrary project** — created by TALXIS **pp-script-library** template and contains script sources (folder `TS`) +- **Solution project** — upgraded to **TALXIS.DevKit.SDK** and built by TALXIS build targets + +Then connect them using a standard `ProjectReference`. + +--- + +## Step-by-step migration + +### 1) Create a new ScriptLibrary project using pp-script-library template + +Create the project using `dotnet new`: + +```console +dotnet new pp-script-library -n UI.Scripts --LibraryName main +``` + +**Result:** +- A new ScriptLibrary project exists in the target folder +- The project already follows the TALXIS standard (SDK/targets/structure) + +--- + +### 2) Move script sources (folder `TS`) into the new project + +Move the **entire `TS` folder** from the old ScriptLibrary structure into the new ScriptLibrary project. + +> The `TS` folder is the only folder you need to migrate. All required build-related structure is already inside `TS`. + +**Result:** +- The new ScriptLibrary project contains the script sources under `TS`. + +--- + +### 3) Remove legacy configuration from old files + +After moving the `TS` folder, remove legacy/old-format configuration that belonged to the previous build pipeline. + +**Result:** +- The new project is not tied to the old configuration and relies on TALXIS build targets. + +--- + +### 4) Reference ScriptLibrary project from the upgraded Solution project + +Open the upgraded Solution project (`*.csproj` in **TALXIS.DevKit.SDK** format) and add a `ProjectReference`: + +```xml + + + +``` + +**Result:** +- The Solution project sees the ScriptLibrary project as a dependency. + +--- + +### 5) Validation + +1. Build the Solution project using the standard repository command dotnrt build. +2. Verify: + - the Solution project contains a `ProjectReference` to the ScriptLibrary project + - the ScriptLibrary project builds successfully as part of the Solution build + +--- + +## Final structure (summary) + +- **ScriptLibrary project** is created via TALXIS **pp-script-library** template → `TS` folder is migrated into it. +- **Solution project** is upgraded to **TALXIS.DevKit.SDK** and references the ScriptLibrary project via `ProjectReference`. diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template.cdsproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template.cdsproj new file mode 100644 index 0000000..2610430 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template.cdsproj @@ -0,0 +1,50 @@ + + + + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\PowerApps + + + + + d376201b-1c2a-422a-b50b-2979e16746fb + v4.6.2 + + net462 + PackageReference + Declarations + FAE04EC0-301F-11D3-BF4B-00C04F79EFBC + FAE04EC0-301F-11D3-BF4B-00C04F79EFBC + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template.csproj new file mode 100644 index 0000000..9e14d86 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template.csproj @@ -0,0 +1,11 @@ + + + net462 + Solution + + + + + + + diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template_talxis.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template_talxis.csproj new file mode 100644 index 0000000..6ce84bc --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/DataverseSolution/template_talxis.csproj @@ -0,0 +1,19 @@ + + + + net462 + latest + PCT21016.Apps.Core.Model + PCT21016.Apps.Core.Model + false + 0.0.20000.0 + 0.0.20000.0 + 0.0.20000.0 + + + + + + + + diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/PDPackage/template.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/PDPackage/template.csproj new file mode 100644 index 0000000..ee3d11f --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/PDPackage/template.csproj @@ -0,0 +1,20 @@ + + + net472 + Package.Main + Package.Main + Copyright © 2026 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/PDPackage/template_new.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/PDPackage/template_new.csproj new file mode 100644 index 0000000..9b00912 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/PDPackage/template_new.csproj @@ -0,0 +1,16 @@ + + + net472 + + + Copyright © 2026 + PDPackage + + + + + + + + + diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/Plugin/template.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/Plugin/template.csproj new file mode 100644 index 0000000..c954b45 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/Plugin/template.csproj @@ -0,0 +1,29 @@ + + + + net462 + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\PowerApps + 1.0.0.0 + 1.0.0.0 + {4C25E9B5-9FA6-436c-8E19-B395D2A65FAF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + + + + + + + + $(FileVersion) + + + This is a sample nuget package which contains a Dataverse plugin and its runtime dependencies like Newtonsoft.Json + + + + + + + + + + diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/Plugin/template_new.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/Plugin/template_new.csproj new file mode 100644 index 0000000..f415704 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/Plugin/template_new.csproj @@ -0,0 +1,22 @@ + + + net462 + true + 1.0.0.0 + 1.0.0.0 + + Plugin + + + + + $(FileVersion) + + + This is a sample nuget package which contains a Dataverse plugin and its runtime dependencies like Newtonsoft.Json + + + + + + diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/ScriptLibrary/template.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/ScriptLibrary/template.csproj new file mode 100644 index 0000000..7bcb8f3 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/ScriptLibrary/template.csproj @@ -0,0 +1,28 @@ + + + net462 + latest + false + $(MSBuildProjectDirectory)\TS + $(TypeScriptDir)\build + true + + + + + + + + + + + PreserveNewest + + + + + + + + + diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Templates/ScriptLibrary/template_new.csproj b/src/TALXIS.CLI.Workspace/Upgrade/Templates/ScriptLibrary/template_new.csproj new file mode 100644 index 0000000..0a183d6 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Templates/ScriptLibrary/template_new.csproj @@ -0,0 +1,12 @@ + + + + net462 + latest + false + $(MSBuildProjectDirectory)\TS + $(TypeScriptDir)\build + true + ScriptLibrary + + diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Utilities/ProjectTypeDetector.cs b/src/TALXIS.CLI.Workspace/Upgrade/Utilities/ProjectTypeDetector.cs new file mode 100644 index 0000000..a404cce --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Utilities/ProjectTypeDetector.cs @@ -0,0 +1,255 @@ +using System.Xml.Linq; +using TALXIS.CLI.Workspace.Upgrade.Models; + +namespace TALXIS.CLI.Workspace.Upgrade.Utilities; + +public class ProjectTypeDetector +{ + private static readonly XNamespace MsbuildNamespace = "http://schemas.microsoft.com/developer/msbuild/2003"; + + public ProjectType DetectProjectType(string filePath) + { + var extension = Path.GetExtension(filePath).ToLowerInvariant(); + + if(extension == ".cdsproj") return ProjectType.DataverseSolution; + + if (extension == ".csproj") + { + return DetectFromContent(filePath); + } + + return ProjectType.Unknown; + } + + private ProjectType DetectFromContent(string filePath) + { + try + { + var xml = XDocument.Load(filePath); + var root = xml.Root; + + if (root == null) return ProjectType.Unknown; + + // Check for PDPackage first (most specific) + if (IsPDPackage(root)) + { + return ProjectType.PDPackage; + } + + // Check for Plugin + if (IsPlugin(root)) + { + return ProjectType.Plugin; + } + + // Check for Script Library + if (IsScriptLibrary(root)) + { + return ProjectType.ScriptLibrary; + } + + // Check for Dataverse Solution + if (IsDataverseSolution(root)) + { + return ProjectType.DataverseSolution; + } + + return ProjectType.Unknown; + } + catch + { + // If we can't parse, fall back to Unknown + return ProjectType.Unknown; + } + } + + private bool IsDataverseSolution(XElement root) + { + // Get all PropertyGroup elements + var propertyGroups = root.Elements(MsbuildNamespace + "PropertyGroup") + .Concat(root.Elements("PropertyGroup")); + + // Check for ProjectType = "Solution" + var projectType = propertyGroups + .Elements(MsbuildNamespace + "ProjectType") + .Concat(propertyGroups.Elements("ProjectType")) + .FirstOrDefault()?.Value; + + if (projectType == "Solution") + return true; + + // Check for SolutionRootPath property + var hasSolutionRootPath = propertyGroups + .Elements(MsbuildNamespace + "SolutionRootPath") + .Concat(propertyGroups.Elements("SolutionRootPath")) + .Any(); + + if (hasSolutionRootPath) + return true; + + // Check for Publisher metadata + var hasPublisherName = propertyGroups + .Elements(MsbuildNamespace + "PublisherName") + .Concat(propertyGroups.Elements("PublisherName")) + .Any(); + + var hasPublisherPrefix = propertyGroups + .Elements(MsbuildNamespace + "PublisherPrefix") + .Concat(propertyGroups.Elements("PublisherPrefix")) + .Any(); + + if (hasPublisherName || hasPublisherPrefix) + return true; + + // Check for PowerApps imports + var imports = root.Elements(MsbuildNamespace + "Import") + .Concat(root.Elements("Import")); + + var hasPowerAppsImport = imports.Any(import => + { + var project = import.Attribute("Project")?.Value ?? ""; + return project.Contains("PowerApps", StringComparison.OrdinalIgnoreCase); + }); + + if (hasPowerAppsImport) + return true; + + // Check for old TALXIS format package reference + var itemGroups = root.Elements(MsbuildNamespace + "ItemGroup") + .Concat(root.Elements("ItemGroup")); + + var hasTalxisSdkReference = itemGroups + .Elements(MsbuildNamespace + "PackageReference") + .Concat(itemGroups.Elements("PackageReference")) + .Any(pkg => + { + var include = pkg.Attribute("Include")?.Value ?? ""; + return include.Contains("TALXIS.SDK.BuildTargets.CDS.Solution", StringComparison.OrdinalIgnoreCase); + }); + + if (hasTalxisSdkReference) + return true; + + return false; + } + + private bool IsPDPackage(XElement root) + { + var itemGroups = root.Elements(MsbuildNamespace + "ItemGroup") + .Concat(root.Elements("ItemGroup")); + + var hasPDPackageReference = itemGroups + .Elements(MsbuildNamespace + "PackageReference") + .Concat(itemGroups.Elements("PackageReference")) + .Any(pkg => + { + var include = pkg.Attribute("Include")?.Value ?? ""; + return include.Contains("TALXIS.PowerApps.MSBuild.PDPackage", StringComparison.OrdinalIgnoreCase) || + include.Contains("TALXIS.SDK.BuildTargets.CDS.Package", StringComparison.OrdinalIgnoreCase) || + include.Contains("Microsoft.PowerApps.MSBuild.PDPackage", StringComparison.OrdinalIgnoreCase); + }); + + return hasPDPackageReference; + } + + private bool IsPlugin(XElement root) + { + var itemGroups = root.Elements(MsbuildNamespace + "ItemGroup") + .Concat(root.Elements("ItemGroup")); + + var hasPluginReference = itemGroups + .Elements(MsbuildNamespace + "PackageReference") + .Concat(itemGroups.Elements("PackageReference")) + .Any(pkg => + { + var include = pkg.Attribute("Include")?.Value ?? ""; + return include.Contains("Microsoft.PowerApps.MSBuild.Plugin", StringComparison.OrdinalIgnoreCase); + }); + + return hasPluginReference; + } + + private bool IsScriptLibrary(XElement root) + { + var propertyGroups = root.Elements(MsbuildNamespace + "PropertyGroup") + .Concat(root.Elements("PropertyGroup")); + + // Check for ProjectType = "ScriptLibrary" (new format) + var projectType = propertyGroups + .Elements(MsbuildNamespace + "ProjectType") + .Concat(propertyGroups.Elements("ProjectType")) + .FirstOrDefault()?.Value; + + if (projectType == "ScriptLibrary") + return true; + + // Check for TypeScript properties + var hasTypeScriptDir = propertyGroups + .Elements(MsbuildNamespace + "TypeScriptDir") + .Concat(propertyGroups.Elements("TypeScriptDir")) + .Any(); + + var hasTypeScriptBuildDir = propertyGroups + .Elements(MsbuildNamespace + "TypeScriptBuildDir") + .Concat(propertyGroups.Elements("TypeScriptBuildDir")) + .Any(); + + if (hasTypeScriptDir || hasTypeScriptBuildDir) + return true; + + // Check for BuildTypeScript target + var targets = root.Elements(MsbuildNamespace + "Target") + .Concat(root.Elements("Target")); + + var hasBuildTypeScriptTarget = targets.Any(target => + { + var name = target.Attribute("Name")?.Value ?? ""; + return name.Equals("BuildTypeScript", StringComparison.OrdinalIgnoreCase); + }); + + if (hasBuildTypeScriptTarget) + return true; + + // Check for npm commands in Exec elements + var execs = targets + .Elements(MsbuildNamespace + "Exec") + .Concat(targets.Elements("Exec")); + + var hasNpmCommand = execs.Any(exec => + { + var command = exec.Attribute("Command")?.Value ?? ""; + return command.Contains("npm", StringComparison.OrdinalIgnoreCase); + }); + + return hasNpmCommand; + } + + public bool IsOldTalxisFormat(string filePath) + { + try + { + var xml = XDocument.Load(filePath); + var root = xml.Root; + + if (root == null) return false; + + var itemGroups = root.Elements(MsbuildNamespace + "ItemGroup") + .Concat(root.Elements("ItemGroup")); + + var hasTalxisSdkReference = itemGroups + .Elements(MsbuildNamespace + "PackageReference") + .Concat(itemGroups.Elements("PackageReference")) + .Any(pkg => + { + var include = pkg.Attribute("Include")?.Value ?? ""; + return include.Contains("TALXIS.SDK.BuildTargets.CDS.Solution", StringComparison.OrdinalIgnoreCase); + }); + + return hasTalxisSdkReference; + } + catch + { + return false; + } + } +} diff --git a/src/TALXIS.CLI.Workspace/Upgrade/Utilities/TemplateManager.cs b/src/TALXIS.CLI.Workspace/Upgrade/Utilities/TemplateManager.cs new file mode 100644 index 0000000..5df1d0d --- /dev/null +++ b/src/TALXIS.CLI.Workspace/Upgrade/Utilities/TemplateManager.cs @@ -0,0 +1,67 @@ +using TALXIS.CLI.Workspace.Upgrade.Models; + +namespace TALXIS.CLI.Workspace.Upgrade.Utilities; + +public class TemplateManager +{ + private readonly string _templatesBasePath; + + public TemplateManager(string templateBasePath) + { + _templatesBasePath = templateBasePath; + } + + public string GetOldFormatTemplatePath(ProjectType projectType, bool isOldTalxisFormat = false) + { + return projectType switch + { + ProjectType.DataverseSolution => isOldTalxisFormat + ? Path.Combine(_templatesBasePath, "DataverseSolution", "template_talxis.csproj") + : Path.Combine(_templatesBasePath, "DataverseSolution", "template.cdsproj"), + ProjectType.ScriptLibrary => Path.Combine(_templatesBasePath, "ScriptLibrary", "template.csproj"), + ProjectType.Plugin => Path.Combine(_templatesBasePath, "Plugin", "template.csproj"), + ProjectType.PDPackage => Path.Combine(_templatesBasePath, "PDPackage", "template.csproj"), + _ => throw new NotSupportedException($"Project type {projectType} is not supported") + }; + } + + public string GetNewFormatTemplatePath(ProjectType projectType) + { + return projectType switch + { + ProjectType.DataverseSolution => Path.Combine(_templatesBasePath, "DataverseSolution", "template.csproj"), + ProjectType.ScriptLibrary => Path.Combine(_templatesBasePath, "ScriptLibrary", "template_new.csproj"), + ProjectType.Plugin => Path.Combine(_templatesBasePath, "Plugin", "template_new.csproj"), + ProjectType.PDPackage => Path.Combine(_templatesBasePath, "PDPackage", "template_new.csproj"), + _ => throw new NotSupportedException($"Project type {projectType} is not supported") + }; + } + + public bool TemplatesExist(ProjectType projectType, bool isOldTalxisFormat = false) + { + try + { + var oldPath = GetOldFormatTemplatePath(projectType, isOldTalxisFormat); + var newPath = GetNewFormatTemplatePath(projectType); + + return File.Exists(oldPath) && File.Exists(newPath); + } + catch + { + return false; + } + } + + public void ValidateTemplates(ProjectType projectType, bool isOldTalxisFormat = false) + { + if (!TemplatesExist(projectType, isOldTalxisFormat)) + { + throw new FileNotFoundException( + $"Template files for {projectType} not found in {_templatesBasePath}. " + + $"Expected files:\n" + + $" - {GetOldFormatTemplatePath(projectType, isOldTalxisFormat)}\n" + + $" - {GetNewFormatTemplatePath(projectType)}" + ); + } + } +} diff --git a/src/TALXIS.CLI.Workspace/UpgradeCliCommand.cs b/src/TALXIS.CLI.Workspace/UpgradeCliCommand.cs new file mode 100644 index 0000000..7786259 --- /dev/null +++ b/src/TALXIS.CLI.Workspace/UpgradeCliCommand.cs @@ -0,0 +1,58 @@ +using DotMake.CommandLine; +using Microsoft.Extensions.Logging; +using System.IO; +using TALXIS.CLI.Workspace.Upgrade; + +namespace TALXIS.CLI.Workspace; + +[CliCommand( + Description = "Upgrade project files to SDK-style format while preserving custom references and backups.", + Name = "upgrade", + Children = new[] { typeof(ProjectUpgradeCliCommand) })] +public class UpgradeCliCommand +{ + [CliArgument(Name = "path", Description = "Path to a .csproj/.cdsproj file or directory to upgrade.")] + public required string TargetPath { get; set; } + + [CliOption(Description = "Skip creating .backup files before rewriting.")] + public bool NoBackup { get; set; } + + public int Run() + { + return RunInternal(TargetPath, NoBackup); + } + + [CliCommand( + Description = "Upgrade project files (explicit project subcommand).", + Name = "project")] + public class ProjectUpgradeCliCommand + { + [CliArgument(Name = "path", Description = "Path to a .csproj/.cdsproj file or directory to upgrade.")] + public required string TargetPath { get; set; } + + [CliOption(Description = "Skip creating .backup files before rewriting.")] + public bool NoBackup { get; set; } + + public int Run() + { + return RunInternal(TargetPath, NoBackup); + } + } + + private static int RunInternal(string targetPath, bool noBackup) + { + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "HH:mm:ss "; + }); + builder.SetMinimumLevel(LogLevel.Information); + }); + + var templatesBasePath = System.IO.Path.Combine(AppContext.BaseDirectory, "Upgrade", "Templates"); + var runner = new ProjectUpgradeRunner(loggerFactory, templatesBasePath, createBackup: !noBackup); + return runner.Run(targetPath); + } +} diff --git a/src/TALXIS.CLI.Workspace/WorkspaceCliCommand.cs b/src/TALXIS.CLI.Workspace/WorkspaceCliCommand.cs index ad758e0..5ab9470 100644 --- a/src/TALXIS.CLI.Workspace/WorkspaceCliCommand.cs +++ b/src/TALXIS.CLI.Workspace/WorkspaceCliCommand.cs @@ -7,7 +7,8 @@ namespace TALXIS.CLI.Workspace; Alias = "ws", Children = new[] { - typeof(ComponentCliCommand) + typeof(ComponentCliCommand), + typeof(UpgradeCliCommand) })] public class WorkspaceCliCommand {