using Matrix42.Extensions.Scaffolder.Core.ProjectGenerators; using System; using System.Collections.Generic; using System.IO; namespace Matrix42.Extensions.Scaffolder.Core; public sealed class ExtensionScaffolder : IExtensionScaffolder { private readonly DefaultProjectGenerator _projectGenerator = new(); public IReadOnlyList Generate(ExtensionScaffoldingOptions options) { if (options is null) throw new ArgumentNullException(nameof(options)); if (string.IsNullOrWhiteSpace(options.ExtensionPath)) throw new ArgumentException("ExtensionPath must be set.", nameof(options)); if (string.IsNullOrWhiteSpace(options.ExtensionNamespace)) throw new ArgumentException("ExtensionNamespace must be set.", nameof(options)); if (options.Projects == null) throw new ArgumentException("Projects must be set.", nameof(options)); var result = new List(); PrepareRootStructure(options); PrepareBasePackage(options); var solutionDir = PrepareSolutionFolder(options); PrepareRootArtifacts(options); PrepareSolutionFiles(options, solutionDir); foreach (var def in options.Projects) { var csprojPath = _projectGenerator.GenerateProject(options, def); result.Add(csprojPath); } GenerateSolutionFile(options, solutionDir, result); return result; } private void PrepareRootStructure(ExtensionScaffoldingOptions options) { Directory.CreateDirectory(options.ExtensionPath); } private void PrepareBasePackage(ExtensionScaffoldingOptions options) { var basePackageDir = Path.Combine(options.ExtensionPath, "BasePackage"); Directory.CreateDirectory(basePackageDir); var packageJsonPath = Path.Combine(basePackageDir, "package.json"); if (!File.Exists(packageJsonPath)) { var json = GetDefaultPackageJson(options); File.WriteAllText(packageJsonPath, json); } } /// /// Creates /Solutions/{ns} and returns that path (solution-level folder). /// private string PrepareSolutionFolder(ExtensionScaffoldingOptions options) { var solutionDir = Path.Combine(options.ExtensionPath, "Solutions", options.ExtensionNamespace); Directory.CreateDirectory(solutionDir); return solutionDir; } private void PrepareRootArtifacts(ExtensionScaffoldingOptions options) { /*var azurePath = Path.Combine(options.ExtensionPath, "azure-pipelines-ci.yml"); if (!File.Exists(azurePath)) { File.WriteAllText(azurePath, Templates.AzurePipelinesYml); }*/ var azurePath = Path.Combine(options.ExtensionPath, "azure-pipelines-ci.yaml"); if (!File.Exists(azurePath)) { var repoName = new DirectoryInfo(options.ExtensionPath).Name; var content = Templates.GetAzurePiplineCi(); var replacedContent = content.Replace("{repoName}", repoName).Replace("{ns}", options.ExtensionNamespace); File.WriteAllText(azurePath, replacedContent); } var gitignorePath = Path.Combine(options.ExtensionPath, ".gitignore"); if (!File.Exists(gitignorePath)) { File.WriteAllText(gitignorePath, Templates.Gitignore); } /*var veracodePath = Path.Combine(options.ExtensionPath, "PrepareVeracodeScan.cmd"); if (!File.Exists(veracodePath)) { var content = Templates.PrepareVeracodeScanCmd.Replace("{ns}", options.ExtensionNamespace); File.WriteAllText(veracodePath, content); }*/ var targetsPath = Path.Combine(options.ExtensionPath, "Extension.Assemblies.targets"); if (!File.Exists(targetsPath)) { var content = Templates.GetMatrix42AssembliesTargets(); File.WriteAllText(targetsPath, content); } } /// /// Solution-level files that should live next to the .sln (e.g. BuildEvent.cmd). /// private void PrepareSolutionFiles(ExtensionScaffoldingOptions options, string solutionDir) { var buildEventPath = Path.Combine(solutionDir, "BuildEvent.cmd"); if (!File.Exists(buildEventPath)) { var content = Templates.BuildEventCmdTemplate.Replace("{ns}", options.ExtensionNamespace); File.WriteAllText(buildEventPath, content); } // If later you want to actually create the .sln here (for console scenario), // this is the right place as well. } private static string GetDefaultPackageJson(ExtensionScaffoldingOptions options) => $@"{{ ""Id"": ""{options.ExtensionId}"", ""Vendor"": ""Not Specified"", ""Name"": ""{options.ExtensionNamespace}"", ""Version"": ""1.0"", ""Prerequisites"": {{ ""MinimalRequiredProductVersion"": ""26.1.0"", ""DependentPackages"": [] }}, ""SetupDirectives"": {{ ""MaintenanceMode"": false, ""RecycleWebApplication"": false, ""RestartM42WindowsServices"": false }}, ""Sandboxed"": true }}"; /// /// Creates {ns}.sln under solutionDir and adds all csproj files to it. /// private void GenerateSolutionFile( ExtensionScaffoldingOptions options, string solutionDir, IReadOnlyList projectPaths) { if (projectPaths == null || projectPaths.Count == 0) return; var solutionName = options.ExtensionNamespace; var slnPath = Path.Combine(solutionDir, solutionName + ".sln"); // Basic VS solution header var lines = new List(); lines.Add("Microsoft Visual Studio Solution File, Format Version 12.00"); lines.Add("# Visual Studio Version 17"); lines.Add("VisualStudioVersion = 17.0.31912.275"); lines.Add("MinimumVisualStudioVersion = 10.0.40219.1"); // C# project type GUID const string csProjTypeGuid = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"; // Collect project GUIDs var projectEntries = new List>(); // (projGuid, projName, relativePath) foreach (var projPath in projectPaths) { var projName = Path.GetFileNameWithoutExtension(projPath); var relativePath = GetRelativePath(solutionDir, projPath); var projGuid = "{" + Guid.NewGuid().ToString().ToUpperInvariant() + "}"; projectEntries.Add(Tuple.Create(projGuid, projName, relativePath)); lines.Add(string.Format( "Project(\"{0}\") = \"{1}\", \"{2}\", \"{3}\"", csProjTypeGuid, projName, relativePath, projGuid)); lines.Add("EndProject"); } // Global section (configs + project configs) lines.Add("Global"); lines.Add("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution"); lines.Add("\t\tDebug|Any CPU = Debug|Any CPU"); lines.Add("\t\tRelease|Any CPU = Release|Any CPU"); lines.Add("\tEndGlobalSection"); lines.Add("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution"); foreach (var entry in projectEntries) { var guid = entry.Item1; lines.Add(string.Format("\t\t{0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU", guid)); lines.Add(string.Format("\t\t{0}.Debug|Any CPU.Build.0 = Debug|Any CPU", guid)); lines.Add(string.Format("\t\t{0}.Release|Any CPU.ActiveCfg = Release|Any CPU", guid)); lines.Add(string.Format("\t\t{0}.Release|Any CPU.Build.0 = Release|Any CPU", guid)); } lines.Add("\tEndGlobalSection"); lines.Add("\tGlobalSection(SolutionProperties) = preSolution"); lines.Add("\t\tHideSolutionNode = FALSE"); lines.Add("\tEndGlobalSection"); lines.Add("EndGlobal"); File.WriteAllLines(slnPath, lines); } /// /// Simple helper to get relative path from one folder to a file. /// private static string GetRelativePath(string basePath, string fullPath) { var baseUri = new Uri(AppendDirectorySeparatorChar(basePath)); var fullUri = new Uri(fullPath); var relative = baseUri.MakeRelativeUri(fullUri).ToString(); return relative.Replace('/', Path.DirectorySeparatorChar); } private static string AppendDirectorySeparatorChar(string path) { return !path.EndsWith(Path.DirectorySeparatorChar.ToString()) ? path + Path.DirectorySeparatorChar : path; } }