Skip to content
Open
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
18 changes: 18 additions & 0 deletions src/TALXIS.CLI.Core/Contracts/Dataverse/ISolutionSyncService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace TALXIS.CLI.Core.Contracts.Dataverse;

public sealed record SolutionSyncOptions(
string SolutionUniqueName,
string SolutionRootPath,
string? ProjectFilePath);

public sealed record SolutionSyncResult(
string SolutionRootPath,
IReadOnlyList<string> NormalizedAssemblies,
IReadOnlyList<string> ExcludedBinaries,
IReadOnlyList<string> ExcludedWebResources,
IReadOnlyList<string> RemovedFiles);

public interface ISolutionSyncService
{
Task<SolutionSyncResult> SyncAsync(string? profileName, SolutionSyncOptions options, CancellationToken ct);
}
94 changes: 94 additions & 0 deletions src/TALXIS.CLI.Core/Resolution/ProjectReferenceReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System.Xml.Linq;

namespace TALXIS.CLI.Core.Resolution;

public static class ProjectReferenceReader
{
private static readonly HashSet<string> PluginProjectTypes =
new(StringComparer.OrdinalIgnoreCase) { "Plugin", "WorkflowActivity" };

public static IReadOnlyCollection<string> ReadPluginAssemblyNames(string projectFilePath)
{
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var referenced in EnumerateReferencedProjects(projectFilePath))
{
var info = ReadProjectInfo(referenced);
if (info.ProjectType is not null && PluginProjectTypes.Contains(info.ProjectType))
result.Add(info.AssemblyName);
}
return result;
}

public static IReadOnlyCollection<string> ReadScriptLibraryWebResourceNames(string solutionProjectFilePath)
{
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var publisherPrefix = ReadProperty(solutionProjectFilePath, "PublisherPrefix");
if (string.IsNullOrWhiteSpace(publisherPrefix))
return result;

foreach (var referenced in EnumerateReferencedProjects(solutionProjectFilePath))
{
var info = ReadProjectInfo(referenced);
if (string.Equals(info.ProjectType, "ScriptLibrary", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrWhiteSpace(info.ScriptLibraryName))
result.Add(info.ScriptLibraryName.StartsWith(publisherPrefix) ? $"{info.ScriptLibraryName}.js" : $"{publisherPrefix}_{info.ScriptLibraryName}.js");
}
return result;
}

private static IEnumerable<string> EnumerateReferencedProjects(string projectFilePath)
{
if (!File.Exists(projectFilePath))
yield break;

var projectDir = Path.GetDirectoryName(Path.GetFullPath(projectFilePath))!;
var doc = XDocument.Load(projectFilePath);
var ns = doc.Root?.Name.Namespace ?? XNamespace.None;

foreach (var reference in doc.Descendants(ns + "ProjectReference"))
{
var include = reference.Attribute("Include")?.Value;
if (string.IsNullOrWhiteSpace(include))
continue;

var relative = include.Replace('\\', Path.DirectorySeparatorChar);
var referencedPath = Path.GetFullPath(Path.Combine(projectDir, relative));
if (File.Exists(referencedPath))
yield return referencedPath;
}
}

private static (string? ProjectType, string AssemblyName, string? ScriptLibraryName) ReadProjectInfo(string referencedProjectPath)
{
var fallbackName = Path.GetFileNameWithoutExtension(referencedProjectPath);
try
{
var doc = XDocument.Load(referencedProjectPath);
var ns = doc.Root?.Name.Namespace ?? XNamespace.None;
var projectType = doc.Descendants(ns + "ProjectType").FirstOrDefault()?.Value?.Trim();
var assemblyName = doc.Descendants(ns + "AssemblyName").FirstOrDefault()?.Value?.Trim();
var scriptLibraryName = doc.Descendants(ns + "ScriptLibraryName").FirstOrDefault()?.Value?.Trim();
return (projectType, string.IsNullOrWhiteSpace(assemblyName) ? fallbackName : assemblyName, scriptLibraryName);
}
catch (System.Xml.XmlException)
{
return (null, fallbackName, null);
}
}

private static string? ReadProperty(string projectFilePath, string propertyName)
{
if (!File.Exists(projectFilePath))
return null;
try
{
var doc = XDocument.Load(projectFilePath);
var ns = doc.Root?.Name.Namespace ?? XNamespace.None;
return doc.Descendants(ns + propertyName).FirstOrDefault()?.Value?.Trim();
}
catch (System.Xml.XmlException)
{
return null;
}
}
}
55 changes: 55 additions & 0 deletions src/TALXIS.CLI.Core/Resolution/SolutionSyncMerge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
namespace TALXIS.CLI.Core.Resolution;

public static class SolutionSyncMerge
{
// Mirrors each top-level folder of the export into the solution root. Top-level entries the
// export doesn't contain (project file, bin, obj, ...) are left alone so they can't be deleted.
public static IReadOnlyList<string> Merge(string fromRoot, string intoRoot)
{
var deleted = new List<string>();
Directory.CreateDirectory(intoRoot);

foreach (var file in Directory.GetFiles(fromRoot))
File.Copy(file, Path.Combine(intoRoot, Path.GetFileName(file)), overwrite: true);

foreach (var dir in Directory.GetDirectories(fromRoot))
MirrorDirectory(dir, Path.Combine(intoRoot, Path.GetFileName(dir)), intoRoot, deleted);

return deleted;
}

private static void MirrorDirectory(string from, string into, string intoRoot, List<string> deleted)
{
Directory.CreateDirectory(into);

var fromFiles = new HashSet<string>(
Directory.GetFiles(from).Select(Path.GetFileName)!, StringComparer.OrdinalIgnoreCase);
var fromDirs = new HashSet<string>(
Directory.GetDirectories(from).Select(Path.GetFileName)!, StringComparer.OrdinalIgnoreCase);

foreach (var file in Directory.GetFiles(from))
File.Copy(file, Path.Combine(into, Path.GetFileName(file)), overwrite: true);

foreach (var sub in Directory.GetDirectories(from))
MirrorDirectory(sub, Path.Combine(into, Path.GetFileName(sub)), intoRoot, deleted);

foreach (var file in Directory.GetFiles(into))
{
if (!fromFiles.Contains(Path.GetFileName(file)))
{
deleted.Add(Path.GetRelativePath(intoRoot, file));
File.Delete(file);
}
}

foreach (var sub in Directory.GetDirectories(into))
{
if (!fromDirs.Contains(Path.GetFileName(sub)))
{
foreach (var f in Directory.GetFiles(sub, "*", SearchOption.AllDirectories))
deleted.Add(Path.GetRelativePath(intoRoot, f));
Directory.Delete(sub, recursive: true);
}
}
}
}
142 changes: 142 additions & 0 deletions src/TALXIS.CLI.Core/Resolution/SolutionSyncTransform.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using System.Xml.Linq;

namespace TALXIS.CLI.Core.Resolution;

public static class SolutionSyncTransform
{
private const string PluginAssembliesDir = "PluginAssemblies";
private const string WebResourcesDir = "WebResources";
private const string DataXmlSuffix = ".data.xml";

public static IReadOnlyList<string> NormalizePluginAssemblyPaths(string unpackedRoot)
{
var normalized = new List<string>();
var pluginsDir = Path.Combine(unpackedRoot, PluginAssembliesDir);
if (!Directory.Exists(pluginsDir))
return normalized;

foreach (var nestedDir in Directory.GetDirectories(pluginsDir))
{
foreach (var file in Directory.GetFiles(nestedDir))
{
var fileName = Path.GetFileName(file);
var destination = Path.Combine(pluginsDir, fileName);

if (File.Exists(destination))
File.Delete(destination);
File.Move(file, destination);

if (fileName.EndsWith(DataXmlSuffix, StringComparison.OrdinalIgnoreCase))
{
var assemblyFileName = fileName[..^DataXmlSuffix.Length];
RewriteFileName(destination, $"/{PluginAssembliesDir}/{assemblyFileName}");
normalized.Add(assemblyFileName);
}
}

if (!Directory.EnumerateFileSystemEntries(nestedDir).Any())
Directory.Delete(nestedDir);
}

return normalized;
}

public static IReadOnlyList<string> ExcludeProjectReferenceBinaries(
string unpackedRoot,
IReadOnlyCollection<string> referencedAssemblyNames)
{
var excluded = new List<string>();
var pluginsDir = Path.Combine(unpackedRoot, PluginAssembliesDir);
if (!Directory.Exists(pluginsDir) || referencedAssemblyNames.Count == 0)
return excluded;

foreach (var dataXml in Directory.GetFiles(pluginsDir, "*" + DataXmlSuffix))
{
var assemblySimpleName = ReadAssemblySimpleName(dataXml);
if (assemblySimpleName is null || !MatchesReference(assemblySimpleName, referencedAssemblyNames))
continue;

var dllName = Path.GetFileName(dataXml)[..^DataXmlSuffix.Length];
var dllPath = Path.Combine(pluginsDir, dllName);
if (File.Exists(dllPath))
{
File.Delete(dllPath);
excluded.Add(dllName);
}
}

return excluded;
}

public static IReadOnlyList<string> ExcludeScriptLibraryWebResources(
string unpackedRoot,
IReadOnlyCollection<string> webResourceNames)
{
var excluded = new List<string>();
var webResDir = Path.Combine(unpackedRoot, WebResourcesDir);
if (!Directory.Exists(webResDir) || webResourceNames.Count == 0)
return excluded;

foreach (var dataXml in Directory.GetFiles(webResDir, "*" + DataXmlSuffix))
{
var resourceName = Path.GetFileName(dataXml)[..^DataXmlSuffix.Length];
if (!webResourceNames.Contains(resourceName))
continue;

var contentPath = Path.Combine(webResDir, resourceName);
if (File.Exists(contentPath))
{
File.Delete(contentPath);
excluded.Add(resourceName);
}
}

return excluded;
}

private static string? ReadAssemblySimpleName(string dataXmlPath)
{
XDocument doc;
try
{
doc = XDocument.Load(dataXmlPath);
}
catch (System.Xml.XmlException)
{
return null;
}

var fullName = doc.Root?.Attribute("FullName")?.Value;
if (string.IsNullOrWhiteSpace(fullName))
return null;

var comma = fullName.IndexOf(',');
return (comma >= 0 ? fullName[..comma] : fullName).Trim();
}

private static bool MatchesReference(string assemblySimpleName, IReadOnlyCollection<string> referencedNames)
{
foreach (var name in referencedNames)
{
if (assemblySimpleName.Equals(name, StringComparison.OrdinalIgnoreCase))
return true;
if (assemblySimpleName.EndsWith("." + name, StringComparison.OrdinalIgnoreCase))
return true;
if (name.EndsWith("." + assemblySimpleName, StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}

private static void RewriteFileName(string dataXmlPath, string newFileName)
{
var doc = XDocument.Load(dataXmlPath);
var ns = doc.Root?.Name.Namespace ?? XNamespace.None;
var fileNameElement = doc.Descendants(ns + "FileName").FirstOrDefault();
if (fileNameElement is null || fileNameElement.Value == newFileName)
return;

fileNameElement.Value = newFileName;
doc.Save(dataXmlPath);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace TALXIS.CLI.Features.Environment.Solution;
Name = "solution",
Alias = "sln",
Description = "Manage solutions in the target environment.",
Children = new[] { typeof(SolutionImportCliCommand), typeof(SolutionUninstallCliCommand), typeof(SolutionDeleteCliCommand), typeof(SolutionListCliCommand), typeof(SolutionGetCliCommand), typeof(SolutionCreateCliCommand), typeof(SolutionExportCliCommand), typeof(SolutionPackCliCommand), typeof(SolutionUnpackCliCommand), typeof(SolutionPublishCliCommand), typeof(SolutionUninstallCheckCliCommand), typeof(Component.SolutionComponentCliCommand) },
Children = new[] { typeof(SolutionImportCliCommand), typeof(SolutionUninstallCliCommand), typeof(SolutionDeleteCliCommand), typeof(SolutionListCliCommand), typeof(SolutionGetCliCommand), typeof(SolutionCreateCliCommand), typeof(SolutionExportCliCommand), typeof(SolutionSyncCliCommand), typeof(SolutionPackCliCommand), typeof(SolutionUnpackCliCommand), typeof(SolutionPublishCliCommand), typeof(SolutionUninstallCheckCliCommand), typeof(Component.SolutionComponentCliCommand) },
ShortFormAutoGenerate = CliNameAutoGenerate.None
)]
public class SolutionCliCommand
Expand Down
Loading