├── tests ├── Program.fs ├── TestProjects │ └── DevAdventures.zip ├── SolutionSnapshotter.Tests.fsproj ├── DevAdventuresTests.fs └── TestUtils.fs ├── src ├── WizardTemplate.zip ├── DefaultWizardIcon.ico ├── DefaultTemplateIcon.ico ├── FodyWeavers.xml ├── PublishManifestGenerator.fs ├── Constants.fs ├── Properties │ └── launchSettings.json ├── input.config ├── CustomFileHandler.fs ├── JsonConverters.fs ├── SolutionInfoParser.fs ├── ExtraFoldersParser.fs ├── MultiProjectVsTemplate.fs ├── SolutionSnapshotter.fsproj ├── ProjectToTemplateConverter.fs ├── SingleProjectVsTemplate.fs ├── StructureGenerator.fs ├── MultiProjectTemplateGenerator.fs ├── UtilTypes.fs ├── Types.fs ├── CLI.fs ├── SingleProjectTemplateGenerator.fs ├── FodyWeavers.xsd ├── Program.fs ├── TemplateWizardGenerator.fs └── Utils.fs ├── vs-ext-development.PNG ├── example-pictures ├── step1.PNG ├── step2.PNG ├── step3.PNG ├── step4.PNG ├── step5.PNG ├── step6.png ├── step7.PNG ├── step8.PNG ├── step9.PNG ├── step10.PNG ├── step11.PNG ├── step12.PNG └── step13.PNG ├── .travis.yml ├── LICENSE ├── azure-pipelines.yml ├── SolutionSnapshotter.sln ├── .gitattributes ├── .gitignore └── README.md /tests/Program.fs: -------------------------------------------------------------------------------- 1 | module Program 2 | 3 | let [] main _ = 0 4 | -------------------------------------------------------------------------------- /src/WizardTemplate.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/src/WizardTemplate.zip -------------------------------------------------------------------------------- /vs-ext-development.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/vs-ext-development.PNG -------------------------------------------------------------------------------- /example-pictures/step1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/example-pictures/step1.PNG -------------------------------------------------------------------------------- /example-pictures/step2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/example-pictures/step2.PNG -------------------------------------------------------------------------------- /example-pictures/step3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/example-pictures/step3.PNG -------------------------------------------------------------------------------- /example-pictures/step4.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/example-pictures/step4.PNG -------------------------------------------------------------------------------- /example-pictures/step5.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/example-pictures/step5.PNG -------------------------------------------------------------------------------- /example-pictures/step6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/example-pictures/step6.png -------------------------------------------------------------------------------- /example-pictures/step7.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/example-pictures/step7.PNG -------------------------------------------------------------------------------- /example-pictures/step8.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/example-pictures/step8.PNG -------------------------------------------------------------------------------- /example-pictures/step9.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/example-pictures/step9.PNG -------------------------------------------------------------------------------- /src/DefaultWizardIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/src/DefaultWizardIcon.ico -------------------------------------------------------------------------------- /example-pictures/step10.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/example-pictures/step10.PNG -------------------------------------------------------------------------------- /example-pictures/step11.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/example-pictures/step11.PNG -------------------------------------------------------------------------------- /example-pictures/step12.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/example-pictures/step12.PNG -------------------------------------------------------------------------------- /example-pictures/step13.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/example-pictures/step13.PNG -------------------------------------------------------------------------------- /src/DefaultTemplateIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/src/DefaultTemplateIcon.ico -------------------------------------------------------------------------------- /tests/TestProjects/DevAdventures.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnikolovv/solution-snapshotter/HEAD/tests/TestProjects/DevAdventures.zip -------------------------------------------------------------------------------- /src/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/PublishManifestGenerator.fs: -------------------------------------------------------------------------------- 1 | module PublishManifestGenerator 2 | 3 | open Types 4 | open Newtonsoft.Json.Linq 5 | open Newtonsoft.Json 6 | open Newtonsoft.Json.Serialization 7 | 8 | let generatePublishManifestJson (args:WizardPublishManifest) = 9 | let jObject = JObject.FromObject(args) 10 | jObject.Add("$schema", JToken.FromObject(Constants.PublishManifestSchema)) 11 | let serializerSettings = new JsonSerializerSettings() 12 | serializerSettings.ContractResolver <- new CamelCasePropertyNamesContractResolver() 13 | let converters = serializerSettings.Converters |> Seq.toArray 14 | let json = jObject.ToString(Formatting.Indented, converters) 15 | json -------------------------------------------------------------------------------- /src/Constants.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Constants 3 | 4 | let PublishManifestSchema = "http://json.schemastore.org/vsix-publish" 5 | let ProjectTemplatesFolder = "ProjectTemplates" 6 | let ExtSafeProjectName = "$ext_safeprojectname$" 7 | let SafeProjectName = "$safeprojectname$" 8 | let VsixIconPlaceholder = "$vsixiconname$" 9 | let DefaultTemplateIcon = "DefaultTemplateIcon.ico" 10 | let DefaultWizardIcon = "DefaultWizardIcon.ico" 11 | let AssemblyName = "SolutionSnapshotter" 12 | let StructureJson = "structure.json" 13 | let TemplateZip = "Template.zip" 14 | let DefaultDestinationFolderName = "generated-template" 15 | let ProgramName = "solution-snapshotter.exe" 16 | let WizardTemplateZip = "WizardTemplate.zip" 17 | 18 | -------------------------------------------------------------------------------- /src/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SolutionSnapshotter": { 4 | "commandName": "Project", 5 | "commandLineArgs": "inline --template-name MyAmazingSetup --template-description \"It's truly amazing.\" --path-to-sln \"MyProject.sln\" --root-project-namespace \"MyProject\" --folders-to-ignore [\"bin\",\".vs\"] --file-extensions-to-ignore [\".csproj.user\"] --vsix-display-name \"MyAmazingSetup Extension\" --vsix-description \"MyAmazingSetup's description\" --vsix-more-info \"https://moreinfo.net\" --vsix-getting-started \"https://gettingstarted.net\" --vsix-price-category free --vsix-qna-enable true --vsix-categories \"other templates\" --vsix-internal-name a-unique-internal-name --vsix-overview-md-path \"..\\overview.md\" --template-wizard-assembly \"MyAmazingSetupWizard\" --vsix-version 1.0 --vsix-publisher-full-name \"Dobromir Nikolov\" --vsix-publisher-username \"dnikolovv\"" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /tests/SolutionSnapshotter.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net472 5 | 6 | false 7 | 8 | 9 | 10 | x64 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | solution: SolutionSnapshotter.sln 3 | 4 | deploy: 5 | provider: releases 6 | script: 7 | - msbuild SolutionSnapshotter.sln -p:Configuration=Release -p:OutputPath=artifacts 8 | skip_cleanup: true 9 | api_key: $GITHUB_TOKEN 10 | file: SolutionSnapshotter/artifacts/solution-snapshotter.exe 11 | on: 12 | tags: true 13 | env: 14 | global: 15 | secure: LQbGAJiua1rC6LRHQVoVxYJhop1vq6TGFRQKI+UprFE7Dtb1VICsvrnfYa/VQAJ0Rk9ZOSDkQzSW/3YH4a2upNkvSanJCfB2QDhwdH8tpQPNVjJzoxmKKBuTm5nQaExA0R2a5U1jGKLEa8mq7ixRxpKxW2ZQqpOQ7ILGNMjaFCGMtfk1Ku8Hi4q0X208n3Qpg5U30m66/aTa9+BRyCzsE2xgwDMGG4Aeu22HTBuA/OXspto07EXAfvgJG/kE9kx7fftS4mVcHLubftzBKmkShhaihrRSIgOCqdboUn+pmWhKFYoxmbgUQv3lkwVrxalyVVNgt8O2Xaco48FP2dbiFBHofrHCg1LOZlKKVBpzKF7I6HiU7gIwiklkEJA2MqMYoPZ+fUD6yDNCT7+c/57Vmzb9JGU+4fcZ3Bri5SdFH5JhsRK2KWdvhJlGzYZiGPxCqUFU4aAP/XxwNKUoLsKH3IW81NjvG/cif0F+uwCMl0qLDeocwUPXcU66F3KJ/nNFgTLe+BrhRb7Kk3lcE1XYpL+Wa/7LQbG4Jezxcj+h3/nYKTnPxF2GQZPxCAfT7IbrkW70ytWQcM1EthgjaaWMSsePB3eyEqNdp5NrADwMmHfmzDUmXiSg5pOfGeNS+uWfeHmA3BkfvHI1VSb16PjpQaD16x1LGeCZsNuLOO9SSQg= 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dobromir Nikolov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Starter pipeline 2 | # Start with a minimal pipeline that you can customize to build and deploy your code. 3 | # Add steps that build, run tests, deploy, and more: 4 | # https://aka.ms/yaml 5 | 6 | pool: 7 | vmImage: "windows-2019" 8 | 9 | variables: 10 | agentBuildDir: $(System.DefaultWorkingDirectory) 11 | 12 | steps: 13 | - task: NuGetToolInstaller@1 14 | inputs: 15 | versionSpec: 16 | displayName: "Install NuGet" 17 | 18 | - task: NuGetCommand@2 19 | inputs: 20 | command: "restore" 21 | restoreSolution: "SolutionSnapshotter.sln" 22 | feedsToUse: "select" 23 | displayName: "Restore packages" 24 | 25 | - task: MSBuild@1 26 | inputs: 27 | solution: "SolutionSnapshotter.sln" 28 | configuration: Release 29 | msbuildVersion: "latest" 30 | displayName: "Build" 31 | 32 | - task: VisualStudioTestPlatformInstaller@1 33 | inputs: 34 | packageFeedSelector: "nugetOrg" 35 | versionSelector: "latestPreRelease" 36 | displayName: "Install tests platform" 37 | 38 | - task: VSTest@2 39 | inputs: 40 | testSelector: "testAssemblies" 41 | testAssemblyVer2: | 42 | **\*Tests.dll 43 | **\*test*.dll 44 | !**\*TestAdapter.dll 45 | !**\obj\** 46 | searchFolder: "$(System.DefaultWorkingDirectory)" 47 | displayName: "Run tests" 48 | -------------------------------------------------------------------------------- /src/input.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SolutionSnapshotter.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29025.244 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "SolutionSnapshotter", "src\SolutionSnapshotter.fsproj", "{AAD44F44-8BF7-4F02-8AC9-E55EB5AFB47C}" 7 | EndProject 8 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "SolutionSnapshotter.Tests", "tests\SolutionSnapshotter.Tests.fsproj", "{580842E4-2DB8-4165-98AA-5A2451F0DDDE}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {AAD44F44-8BF7-4F02-8AC9-E55EB5AFB47C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {AAD44F44-8BF7-4F02-8AC9-E55EB5AFB47C}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {AAD44F44-8BF7-4F02-8AC9-E55EB5AFB47C}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {AAD44F44-8BF7-4F02-8AC9-E55EB5AFB47C}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {580842E4-2DB8-4165-98AA-5A2451F0DDDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {580842E4-2DB8-4165-98AA-5A2451F0DDDE}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {580842E4-2DB8-4165-98AA-5A2451F0DDDE}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {580842E4-2DB8-4165-98AA-5A2451F0DDDE}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {96F2E742-49C7-4D5A-93A0-9E6CAE6F921D} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/CustomFileHandler.fs: -------------------------------------------------------------------------------- 1 | module CustomFileHandler 2 | 3 | open Utils 4 | open UtilTypes 5 | open System.IO 6 | 7 | let private saveFileToDestination fileName (stream:Stream) destination = 8 | let pathToSaveTo = Path.Combine(destination, fileName) 9 | use saveFileStream = File.OpenWrite(pathToSaveTo) 10 | stream.CopyTo(saveFileStream) 11 | 12 | let private saveCustomFile customFilePath destination = 13 | let path = 14 | toRootedPath customFilePath 15 | |> ExistingFilePath.create 16 | |> ExistingFilePath.value 17 | 18 | let fileName = Path.GetFileName(path) 19 | let fileAsStream = File.OpenRead(path) 20 | saveFileToDestination fileName fileAsStream destination 21 | fileName 22 | 23 | /// 24 | /// Copies a custom file to a destination and returns its name. (not path) 25 | /// 26 | let setCustomFile customFilePath (destination:ExistingDirPath) = 27 | let destination = destination |> ExistingDirPath.value 28 | saveCustomFile customFilePath destination 29 | 30 | /// 31 | /// Copies a given file to a destination and returns its name or a default one. 32 | /// Assumes that the default file given is embedded in the assembly under the same name. 33 | /// Useful for stuff like custom icons, logos, readme files, etc. 34 | /// 35 | let setCustomFileOrEmbeddedDefault customFilePath defaultFileName (destination:ExistingDirPath) = 36 | 37 | let destination = destination |> ExistingDirPath.value 38 | 39 | match customFilePath with 40 | | Some path -> saveCustomFile path destination 41 | | None -> 42 | let fileName = defaultFileName 43 | use fileAsStream = getEmbeddedResourceStream fileName 44 | saveFileToDestination fileName fileAsStream destination 45 | fileName -------------------------------------------------------------------------------- /src/JsonConverters.fs: -------------------------------------------------------------------------------- 1 | module JsonConverters 2 | 3 | open Newtonsoft.Json 4 | open System 5 | open Microsoft.FSharp.Reflection 6 | 7 | type UnionJsonConverter () = 8 | inherit JsonConverter() 9 | override this.CanConvert objectType = true; 10 | 11 | override this.WriteJson (writer: JsonWriter, value: obj, serializer: JsonSerializer): unit = 12 | writer.WriteValue(value.ToString().ToLower()) 13 | 14 | override this.CanRead = false 15 | 16 | override this.ReadJson (reader: JsonReader, objectType: Type, existingValue: obj, serializer: JsonSerializer) : obj = 17 | raise (new NotImplementedException()); 18 | 19 | /// Courtesy of https://stackoverflow.com/questions/28150908/f-json-webapi-serialization-of-option-types 20 | type OptionConverter() = 21 | inherit JsonConverter() 22 | 23 | override x.CanConvert(t) = 24 | t.IsGenericType && t.GetGenericTypeDefinition() = typedefof> 25 | 26 | override x.WriteJson(writer, value, serializer) = 27 | let value = 28 | if value = null then null 29 | else 30 | let _,fields = FSharpValue.GetUnionFields(value, value.GetType()) 31 | fields.[0] 32 | serializer.Serialize(writer, value) 33 | 34 | override x.ReadJson(reader, t, existingValue, serializer) = 35 | let innerType = t.GetGenericArguments().[0] 36 | let innerType = 37 | if innerType.IsValueType then (typedefof>).MakeGenericType([|innerType|]) 38 | else innerType 39 | let value = serializer.Deserialize(reader, innerType) 40 | let cases = FSharpType.GetUnionCases(t) 41 | if value = null then FSharpValue.MakeUnion(cases.[0], [||]) 42 | else FSharpValue.MakeUnion(cases.[1], [|value|]) -------------------------------------------------------------------------------- /src/SolutionInfoParser.fs: -------------------------------------------------------------------------------- 1 | module SolutionInfoParser 2 | 3 | open System.IO 4 | open System.Collections.Generic 5 | open Utils 6 | open Microsoft.Build.Construction 7 | open UtilTypes 8 | open Types 9 | 10 | let private toProjectInfo (solution:SolutionFile) (projectId, projectFile) : ProjectInfo = 11 | let getByGuid projectGuid = 12 | if projectGuid = null 13 | then null 14 | else 15 | let (_, value) = 16 | solution 17 | .ProjectsByGuid 18 | .TryGetValue(projectGuid) 19 | value 20 | 21 | let getSolutionDestinationPath projectId = 22 | let project = solution.ProjectsByGuid.[projectId] 23 | let mutable parent = getByGuid project.ParentProjectGuid 24 | let parentsQueue = new Queue<_>() 25 | 26 | while parent <> null do 27 | parentsQueue.Enqueue(parent) 28 | parent <- getByGuid parent.ParentProjectGuid 29 | 30 | let parentNames = 31 | parentsQueue 32 | |> Seq.map (fun p -> p.ProjectName) 33 | |> reverse 34 | 35 | parentNames 36 | |> String.concat "\\" 37 | 38 | let solutionDestinationPath = getSolutionDestinationPath projectId 39 | 40 | { ProjectFile = projectFile 41 | SolutionDestinationPath = RelativePath.create solutionDestinationPath } 42 | 43 | /// 44 | /// Retrieves information about projects inside a .sln file. 45 | /// Does not include solution folders. 46 | /// 47 | let parseProjectInfo pathToSln = 48 | let pathToSln = pathToSln |> ExistingFilePath.value 49 | 50 | let solution = SolutionFile.Parse(pathToSln) 51 | 52 | let projectFiles = 53 | solution.ProjectsInOrder 54 | |> Seq.filter (fun p -> p.ProjectType <> SolutionProjectType.SolutionFolder) 55 | |> Seq.map (fun p -> (p.ProjectGuid, p.AbsolutePath |> FileInfo |> ExistingFile.wrap)) 56 | 57 | projectFiles 58 | |> Seq.map (toProjectInfo solution) 59 | |> List.ofSeq -------------------------------------------------------------------------------- /src/ExtraFoldersParser.fs: -------------------------------------------------------------------------------- 1 | module ExtraFoldersParser 2 | 3 | open System 4 | open System.IO 5 | open Utils 6 | open Types 7 | open UtilTypes 8 | 9 | let private shouldIgnoreExtraFolder (directory:DirectoryInfo) fileExtensionsToIgnore = 10 | let files = directory.GetFiles() 11 | // We don't care about empty extra folders 12 | files.Length = 0 || 13 | // Nor do we care about extra folders that contain only ignored files 14 | files |> Seq.forall (fun f -> fileExtensionsToIgnore |> List.contains f.Extension) 15 | 16 | /// 17 | /// Retrieves extra (non-project) folders. 18 | /// An extra folder is also the "rootProjectPath" if it contains files other than the ignored extensions (e.g. .gitignore, .dockerignore, etc. that are next to the .sln). 19 | /// 20 | let findAndCopyExtraFolders rootProjectPath destination (foldersToIgnore:string list) (fileExtensionsToIgnore:string list) = 21 | let rootProjectPath = rootProjectPath |> ExistingDirPath.value 22 | 23 | let destination = destination |> ExistingDirPath.value 24 | 25 | scanForDirectoriesThatDoNotContain rootProjectPath ["*.*sproj"] foldersToIgnore 26 | |> Seq.filter (fun d -> not <| shouldIgnoreExtraFolder d fileExtensionsToIgnore) 27 | |> Seq.map (fun d -> 28 | // To avoid name collisions (e.g. multiple "configuration" folders throughout the structure) 29 | // we need the name to be unique, thus we append a substring of a random GUID to it 30 | let extraContentsFolderName = sprintf "%s_%s" d.Name (Guid.NewGuid().ToString().Substring(0, 5)) 31 | 32 | let destinationToCopyContentsTo = 33 | Path.Combine(destination, extraContentsFolderName) 34 | |> ExistingDirPath.createFromNonExistingSafe 35 | 36 | copyDirectory 37 | d.FullName 38 | destinationToCopyContentsTo 39 | false 40 | foldersToIgnore 41 | fileExtensionsToIgnore 42 | 43 | let folderPath = d.FullName |> cutStart rootProjectPath 44 | 45 | { FolderPath = folderPath 46 | ContentPath = extraContentsFolderName } 47 | ) 48 | |> List.ofSeq 49 | 50 | -------------------------------------------------------------------------------- /src/MultiProjectVsTemplate.fs: -------------------------------------------------------------------------------- 1 | /// 2 | /// Holds logic for generating the content of a multi-project .vstemplate file. 3 | /// 4 | module MultiProjectVsTemplate 5 | 6 | open System.Xml 7 | open Types 8 | open Utils 9 | open System 10 | 11 | let private rootXmlNamespace = "http://schemas.microsoft.com/developer/vstemplate/2005" 12 | 13 | let private getInitialTemplateString (args:MultiProjectTemplateArgs) = 14 | sprintf @" 15 | 16 | 17 | %s 18 | ProjectName 19 | %s 20 | CSharp 21 | %s 22 | 23 | 24 | 25 | 26 | 27 | 28 | %s, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null 29 | %s.Wizard 30 | 31 | " rootXmlNamespace args.TemplateName args.TemplateDescription args.TemplateIcon args.TemplateWizardAssembly args.TemplateWizardAssembly 32 | 33 | let private toLinkXmlNode document (link:ProjectTemplateLink) = 34 | let node = 35 | createNode 36 | document 37 | "ProjectTemplateLink" 38 | link.VsTemplatePath 39 | rootXmlNamespace 40 | [("ProjectName", link.SafeProjectName); ("CopyParameters", "true")] 41 | document, node 42 | 43 | let private generateProjectXmlLinkNodes document links (projectCollectionNode:XmlNode) = 44 | List.iter (fun link -> 45 | let (_, node) = toLinkXmlNode document link 46 | projectCollectionNode.AppendChild(node) |> ignore 47 | ) links 48 | document, projectCollectionNode 49 | 50 | /// 51 | /// Generates a multi-project .vstemplate file contents. 52 | /// Does not write it to disk. 53 | /// 54 | let getTemplate (args:MultiProjectTemplateArgs) : MultiProjectVsTemplate = 55 | let initialXmlString = getInitialTemplateString args 56 | let document = toXmlDocument initialXmlString 57 | let projectCollectionNode = document.ChildNodes |> findNode "ProjectCollection" 58 | 59 | match projectCollectionNode with 60 | | Some node -> 61 | let (document, _) = 62 | node 63 | |> generateProjectXmlLinkNodes document args.Projects 64 | { Xml = document.OuterXml 65 | LinkElements = args.Projects 66 | InnerProjectsTemplateInfo = [] } 67 | | None -> raise (InvalidOperationException "Could not find the ProjectCollection node. Most likely the initial template string is broken.") -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /tests/DevAdventuresTests.fs: -------------------------------------------------------------------------------- 1 | module Tests 2 | 3 | open System 4 | open Xunit 5 | open Program 6 | open System.IO 7 | open UtilTypes 8 | open Argu 9 | open CLI 10 | open Types 11 | 12 | let setupName = "DevAdventures" 13 | let solutionFileName = "MyProject.sln" 14 | let templateWizardName = "MyAmazingWizard" 15 | 16 | let extractSetup = 17 | extractTestProjectSetup 18 | setupName 19 | solutionFileName 20 | 21 | [] 22 | let ``Dev Adventures Project Setup`` () = 23 | let constructArgs sourceSlnFile destinationFolder = 24 | let argsList = 25 | [ Template_Name "MyAmazingSetup"; 26 | Template_Description "It's truly amazing."; 27 | Root_Project_Namespace "MyProject"; 28 | Folders_To_Ignore ["toBeIgnored"]; 29 | File_Extensions_To_Ignore [".csproj.user"]; 30 | Vsix_Display_Name "MyAmazingSetup Extension"; 31 | Vsix_Description "MyAmazingSetup's description"; 32 | Vsix_More_Info "https://moreinfo.net"; 33 | Vsix_Getting_Started "https://gettingstarted.net"; 34 | Vsix_Price_Category VsixPriceCategory.Free; 35 | Vsix_Qna_Enable true; 36 | Vsix_Categories ["other templates"]; 37 | Vsix_Internal_Name "unique-internal-name"; 38 | Template_Wizard_Assembly templateWizardName; 39 | Vsix_Version "1.0"; 40 | Vsix_Publisher_Full_Name "Dobromir Nikolov"; 41 | Vsix_Publisher_Username "dnikolovv"; 42 | Path_To_Sln (sprintf "%s" sourceSlnFile); 43 | Destination destinationFolder; 44 | Vsix_Overview_Md_Path (sprintf "%s" (Path.Combine(Path.GetDirectoryName(sourceSlnFile), "overview.md")))] 45 | ArgumentParser.Create().ToParseResults(argsList) 46 | 47 | let assertGeneratedTemplateIsCorrect templateFolder vsixFolder args = 48 | let checkVsTemplate vsTemplate = true 49 | let checkStructureJson structure = true 50 | 51 | let checkTemplateFolderContents = 52 | templateFolder 53 | |> DirectoryInfo 54 | |> shouldContainFolders 55 | ["MyProject.Api"; 56 | "MyProject.Core"; 57 | "MyProject.Business"; 58 | "MyProject.Data"; 59 | "MyProject.Data.EntityFramework";] 60 | |> shouldNotContainFolders 61 | ["toBeIgnored*"; 62 | "toBeAlsoIgnored*"] 63 | |> shouldContainFileWithPredicate "*.vstemplate" checkVsTemplate 64 | |> shouldContainFileWithPredicate "structure.json" checkStructureJson 65 | |> shouldContainFile "Template.zip" 66 | |> shouldContainFile "*.ico" 67 | |> ignore 68 | 69 | checkTemplateFolderContents 70 | 71 | generateSetupAndDo 72 | setupName 73 | solutionFileName 74 | constructArgs 75 | assertGeneratedTemplateIsCorrect -------------------------------------------------------------------------------- /src/SolutionSnapshotter.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net472 6 | solution-snapshotter 7 | win 8 | solution-snapshotter 9 | Dobromir Nikolov (dnikolovv) 10 | Dobromir Nikolov (dnikolovv) 11 | A solution template generator for .NET projects. 12 | Dobromir Nikolov (dnikolovv) 13 | Take a snapshot of your solution and instantly export it as a Visual Studio extension. 14 | 0.2.0 15 | https://github.com/dnikolovv/solution-snapshotter 16 | git 17 | https://github.com/dnikolovv/solution-snapshotter 18 | 19 | 20 | 21 | 4 22 | 23 | 24 | 25 | 26 | 0 27 | AnyCPU 28 | 29 | 30 | 31 | 32 | Always 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/ProjectToTemplateConverter.fs: -------------------------------------------------------------------------------- 1 | module ProjectToTemplateConverter 2 | 3 | open UtilTypes 4 | open Newtonsoft.Json 5 | open Microsoft.Build.Construction 6 | open System.IO 7 | open Types 8 | open System 9 | open System.IO.Compression 10 | 11 | let private zipTemplate (templateFolder:ExistingDirPath) = 12 | let templateFolder = templateFolder |> ExistingDirPath.value 13 | let tempPath = Path.Combine(Directory.GetCurrentDirectory(), sprintf "temp-%s" (Guid.NewGuid().ToString().Substring(0, 5))) 14 | Directory.CreateDirectory(tempPath) |> ignore 15 | let tempZipPath = Path.Combine(tempPath, (sprintf "Template-%s.zip" (Guid.NewGuid().ToString().Substring(0, 5)))) 16 | ZipFile.CreateFromDirectory(templateFolder, tempZipPath) 17 | let zipDestination = Path.Combine(templateFolder, Constants.TemplateZip) 18 | File.Move(tempZipPath, zipDestination) 19 | Directory.Delete(tempPath) 20 | zipDestination 21 | 22 | let private writeStructureFile (templateDestination:ExistingDirPath) structureConfiguration = 23 | let templateDestination = templateDestination |> ExistingDirPath.value 24 | let structureJson = 25 | JsonConvert.SerializeObject( 26 | structureConfiguration, 27 | Formatting.Indented) 28 | 29 | File.WriteAllText(Path.Combine(templateDestination, Constants.StructureJson), structureJson) 30 | 31 | let convertProject args = 32 | let projectsDestinationInfo = 33 | SolutionInfoParser 34 | .parseProjectInfo args.PathToSln 35 | 36 | let template = 37 | MultiProjectTemplateGenerator.generateVsTemplateFile 38 | SingleProjectTemplateGenerator.generateTemplate 39 | { TemplateName = args.TemplateName 40 | TemplateDescription = args.TemplateDescription 41 | TemplateIcon = args.IconName 42 | TemplateWizardAssembly = args.TemplateWizardAssembly 43 | Destination = args.TemplateDestination 44 | RootProjectNamespace = args.RootProjectNamespace 45 | FoldersToIgnore = args.FoldersToIgnore 46 | ProjectsDestinationInfo = projectsDestinationInfo 47 | Projects = [] } 48 | 49 | // We don't need the original .sln 50 | // and since the root folder is treated as an "extra" 51 | // we need to explicitly ignore it 52 | let fileExtensionsInExtraFoldersToIgnore = ".sln" :: args.FileExtensionsInExtraFoldersToIgnore 53 | 54 | let extraFolders = 55 | ExtraFoldersParser.findAndCopyExtraFolders 56 | args.RootProjectPath 57 | args.TemplateDestination 58 | args.FoldersToIgnore 59 | fileExtensionsInExtraFoldersToIgnore 60 | 61 | let solution = SolutionFile.Parse(args.PathToSln |> ExistingFilePath.value) 62 | 63 | let structureConfiguration = 64 | StructureGenerator.generateStructureConfiguration 65 | args.RootProjectPath 66 | extraFolders 67 | template.Projects 68 | solution 69 | 70 | writeStructureFile args.TemplateDestination structureConfiguration |> ignore 71 | 72 | zipTemplate args.TemplateDestination 73 | |> ExistingFilePath.create -------------------------------------------------------------------------------- /src/SingleProjectVsTemplate.fs: -------------------------------------------------------------------------------- 1 | /// 2 | /// Holds logic for generating the content of a single project .vstemplate file. 3 | /// 4 | module SingleProjectVsTemplate 5 | 6 | open Types 7 | open Utils 8 | open System.IO 9 | open System.Xml 10 | 11 | let private rootXmlNamespace = "http://schemas.microsoft.com/developer/vstemplate/2005" 12 | 13 | let private initialRootTemplate = 14 | sprintf @" 15 | 16 | 17 | true 18 | 1000 19 | true 20 | Enabled 21 | true 22 | true 23 | 24 | 25 | 26 | " rootXmlNamespace 27 | 28 | let private setProjectRootNode projFileName document= 29 | let projectRootNode = 30 | createNode document "Project" "" rootXmlNamespace [ 31 | ("TargetFileName", projFileName); 32 | ("File", projFileName); 33 | ("ReplaceParameters", "true") ] 34 | 35 | let templateContentNode = document.ChildNodes |> findNode "TemplateContent" |> Option.defaultValue null 36 | let projectRootNode = templateContentNode.AppendChild(projectRootNode) :?> XmlElement 37 | projectRootNode 38 | 39 | 40 | let private setProjectItemNodes (rootProjectFiles:string list) (folderHierarchies:FolderNode list) document (projectRootNode:XmlElement) = 41 | let getProjectItemNode fileName = createNode document "ProjectItem" fileName rootXmlNamespace [ 42 | ("ReplaceParameters", "true"); 43 | ("TargetFileName", fileName) ] 44 | 45 | let rec toXmlFolderNode (folderNode:FolderNode) : XmlElement = 46 | let folderXmlNode = createNode document "Folder" "" rootXmlNamespace [ 47 | ("Name", folderNode.Name); 48 | ("TargetFolderName", folderNode.Name) ] 49 | 50 | List.iter (fun (file:FileInfo) -> 51 | folderXmlNode.AppendChild(getProjectItemNode file.Name) 52 | |> ignore 53 | ) folderNode.Files 54 | 55 | List.iter (fun childFolder -> 56 | folderXmlNode.AppendChild(toXmlFolderNode childFolder) 57 | |> ignore 58 | ) folderNode.ChildFolders 59 | 60 | folderXmlNode 61 | 62 | let projectItemNodes = 63 | rootProjectFiles 64 | |> List.map (fun fileName -> 65 | let projectItemNode = getProjectItemNode fileName 66 | projectRootNode.AppendChild(projectItemNode) 67 | ) 68 | 69 | let folderNodes = 70 | folderHierarchies 71 | |> List.map (fun folderNode -> 72 | let folderXmlNode = toXmlFolderNode folderNode 73 | projectRootNode.AppendChild(folderXmlNode) 74 | ) 75 | 76 | document 77 | 78 | /// 79 | /// Generates a single-project .vstemplate contents. 80 | /// Does not write it to disk. 81 | /// 82 | let getTemplate rootProjectFiles folderHierarchies projFileName = 83 | 84 | let xmlDocument = toXmlDocument initialRootTemplate 85 | 86 | let documentXml = 87 | xmlDocument 88 | |> setProjectRootNode projFileName 89 | |> setProjectItemNodes rootProjectFiles folderHierarchies xmlDocument 90 | |> fun x -> x.OuterXml 91 | 92 | { Xml = documentXml } 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/StructureGenerator.fs: -------------------------------------------------------------------------------- 1 | module StructureGenerator 2 | 3 | open Types 4 | open Utils 5 | open System.IO 6 | open Microsoft.Build.Construction 7 | open UtilTypes 8 | open System 9 | 10 | let rec private buildNodeFromFolder (folder:ProjectInSolution) (allFolders:List) pathUntilNow = 11 | let node = 12 | { Name = folder.ProjectName 13 | FullPath = sprintf "%s%s" pathUntilNow folder.ProjectName 14 | Children = [] } 15 | 16 | let children = 17 | allFolders 18 | |> Seq.filter (fun f -> f.ParentProjectGuid = folder.ProjectGuid) 19 | 20 | let newChildren = 21 | children 22 | |> Seq.map (fun childFolder -> buildNodeFromFolder childFolder allFolders (sprintf "%s\\" node.FullPath)) 23 | |> List.ofSeq 24 | 25 | { node with Children = newChildren } 26 | 27 | let private parseSolutionFolderStructure (solution:SolutionFile) : SolutionFolderStructure = 28 | let allFolders = 29 | solution.ProjectsInOrder 30 | |> Seq.filter (fun p -> p.ProjectType = SolutionProjectType.SolutionFolder) 31 | |> List.ofSeq 32 | 33 | let rootFolders = 34 | allFolders 35 | |> Seq.filter (fun f -> f.ParentProjectGuid = null) 36 | |> List.ofSeq 37 | 38 | let nodes = 39 | rootFolders 40 | |> List.map (fun f -> buildNodeFromFolder f allFolders "") 41 | 42 | { Nodes = nodes } 43 | 44 | let private getProjectsDestinationInfo rootProjectPath projectTemplates : List = 45 | let toProjectDestinationInfoDto (project:ProjectTemplateInfo) = 46 | let trimSlashes str = 47 | str 48 | |> trimEnd ['\\'; '/'] 49 | 50 | let originalProjFilePath = 51 | project.OriginalProjFilePath 52 | |> ExistingFilePath.value 53 | |> fun p -> Uri(p, UriKind.Absolute) 54 | 55 | let rootProjectPath = 56 | rootProjectPath 57 | |> trimSlashes 58 | // It must end with \ in order for MakeRelativeUri to work properly 59 | |> fun s -> s + "\\" 60 | |> fun p -> Uri(p, UriKind.Absolute) 61 | 62 | let destinationDirectory = 63 | rootProjectPath.MakeRelativeUri(originalProjFilePath).ToString() 64 | |> cutEnd (Path.GetFileName(originalProjFilePath.ToString())) 65 | |> trimSlashes 66 | |> cutEnd project.OriginalProjectName 67 | |> trimSlashes 68 | 69 | { ProjectName = project.OriginalProjectName 70 | SafeProjectName = project.SafeProjectName 71 | DestinationDirectory = destinationDirectory 72 | DestinationSolutionDirectory = project.SolutionFolderDestinationPath |> RelativePath.value 73 | ProjectFileExtension = originalProjFilePath.AbsolutePath |> FileInfo |> fun f -> f.Extension } 74 | 75 | projectTemplates 76 | |> List.map toProjectDestinationInfoDto 77 | 78 | let generateStructureConfiguration rootProjectPath extraFolders projectTemplates solution : StructureConfigurationDto = 79 | let rootProjectPath = rootProjectPath |> ExistingDirPath.value 80 | let projectsDestinationInfo = getProjectsDestinationInfo rootProjectPath projectTemplates 81 | let solutionStructure = parseSolutionFolderStructure solution 82 | 83 | { Projects = projectsDestinationInfo 84 | ExtraFolders = extraFolders 85 | SolutionFolderStructure = solutionStructure } 86 | 87 | -------------------------------------------------------------------------------- /src/MultiProjectTemplateGenerator.fs: -------------------------------------------------------------------------------- 1 | module MultiProjectTemplateGenerator 2 | 3 | open Utils 4 | open Types 5 | open System.IO 6 | open UtilTypes 7 | 8 | let private toProjectLink (destination:string) (projectTemplateInfo:ProjectTemplateInfo) : ProjectTemplateLink = 9 | let vsTemplatePath = 10 | projectTemplateInfo.VsTemplatePath 11 | |> ExistingFilePath.value 12 | |> cutStart (destination.TrimEnd('\\')) 13 | |> trimStart '\\' 14 | 15 | let safeProjectName = projectTemplateInfo.OriginalProjectName.Replace(projectTemplateInfo.RootNamespace, Constants.SafeProjectName) 16 | 17 | { VsTemplatePath = vsTemplatePath 18 | OriginalProjectName = projectTemplateInfo.OriginalProjectName 19 | OriginalProjFilePath = ExistingFilePath.value projectTemplateInfo.OriginalProjFilePath 20 | SafeProjectName = safeProjectName } 21 | 22 | let private generateProjectLinkElements projectTemplates destination = 23 | projectTemplates 24 | |> List.map (toProjectLink destination) 25 | 26 | let private generateSingleTemplate generator (args:MultiProjectTemplateArgs) projectInfo = 27 | let destination = args.Destination |> ExistingDirPath.value 28 | let pathToProjFile = 29 | projectInfo.ProjectFile 30 | |> ExistingFile.value 31 | |> fun f -> f.FullName 32 | |> ExistingFilePath.create 33 | 34 | let projFileName = 35 | pathToProjFile 36 | |> ExistingFilePath.getFileName 37 | |> cutEnd ".csproj" 38 | |> cutEnd ".fsproj" 39 | 40 | let newTemplateDestination = 41 | Path.Combine(destination, projFileName) 42 | |> ExistingDirPath.createFromNonExistingSafe 43 | 44 | generator 45 | { ProjFilePath = pathToProjFile 46 | PhysicalDestination = newTemplateDestination 47 | RootProjectNamespace = args.RootProjectNamespace 48 | FoldersToIgnore = args.FoldersToIgnore 49 | SolutionDestinationPath = RelativePath.value projectInfo.SolutionDestinationPath } 50 | 51 | let private generateIndividualTemplates generator args = 52 | args.ProjectsDestinationInfo 53 | |> List.map (generateSingleTemplate generator args) 54 | 55 | let private generateTemplateXml singleProjectTemplateGenerator (args:MultiProjectTemplateArgs) = 56 | let destination = args.Destination |> ExistingDirPath.value 57 | let templates = generateIndividualTemplates singleProjectTemplateGenerator args 58 | let linkElements = generateProjectLinkElements templates destination 59 | let args = { args with Projects = linkElements } 60 | { MultiProjectVsTemplate.getTemplate args with InnerProjectsTemplateInfo = templates } 61 | 62 | let generateVsTemplateFile singleProjectTemplateGenerator (args:MultiProjectTemplateArgs) : MultiProjectTemplateInfo = 63 | 64 | let destination = args.Destination |> ExistingDirPath.value 65 | 66 | let vsTemplateName = sprintf "%s.vstemplate" args.TemplateName 67 | 68 | let vsTemplateDestination = 69 | Path.Combine( 70 | destination, 71 | vsTemplateName) 72 | 73 | let multiProjectTemplate = generateTemplateXml singleProjectTemplateGenerator args 74 | 75 | File.WriteAllText(vsTemplateDestination, multiProjectTemplate.Xml) 76 | 77 | { VsTemplatePath = ExistingFilePath.create vsTemplateDestination 78 | RootDestinationPath = ExistingDirPath.create destination 79 | Projects = multiProjectTemplate.InnerProjectsTemplateInfo } -------------------------------------------------------------------------------- /src/UtilTypes.fs: -------------------------------------------------------------------------------- 1 | module UtilTypes 2 | 3 | open System 4 | open System.IO 5 | 6 | type ExistingDirPath = private ExistingDirPath of string 7 | 8 | type ExistingDir = private ExistingDir of DirectoryInfo 9 | 10 | type ExistingFilePath = private ExistingFilePath of string 11 | 12 | type RelativePath = private RelativePath of string 13 | 14 | type ExistingFile = private ExistingFile of FileInfo 15 | 16 | type String50 = private String50 of string 17 | 18 | [] 19 | module String50 = 20 | 21 | let create (str:string) = 22 | if str.Length > 50 then 23 | raise (ArgumentException (sprintf "Expected '%s' to be less than 50 characters, but was %d." str str.Length)) 24 | String50 str 25 | 26 | let value (String50 str) = str 27 | 28 | [] 29 | module ExistingDir = 30 | 31 | let create (dir:DirectoryInfo) = 32 | if not <| dir.Exists then 33 | raise (new ArgumentException (sprintf "%s should've been an existing directory, but wasn't." dir.FullName)) 34 | else 35 | ExistingDir dir 36 | 37 | let fromString str = create (DirectoryInfo str) 38 | 39 | let fromPath (ExistingDirPath path) = fromString path 40 | 41 | let fromUnexistingPath path = 42 | Directory.CreateDirectory(path) |> ignore 43 | fromString path 44 | 45 | let value (ExistingDir dir) = dir 46 | 47 | let map f (ExistingDir dir) = f dir 48 | 49 | [] 50 | module ExistingDirPath = 51 | 52 | let create path = 53 | if not <| Directory.Exists(path) then 54 | raise (new ArgumentException (sprintf "%s should've been an existing path, but wasn't." path)) 55 | else 56 | ExistingDirPath path 57 | 58 | let length (ExistingDirPath path) = path.Length 59 | 60 | let createFromNonExistingSafe path = 61 | Directory.CreateDirectory(path) |> ignore 62 | create path 63 | 64 | let createFromNonExistingOrFail path = 65 | if Directory.Exists(path) then 66 | raise (new ArgumentException (sprintf "%s should've been an unexisting path, but wasn't." path)) 67 | createFromNonExistingSafe path 68 | 69 | let combineWith pathOrFileName (ExistingDirPath path) = 70 | Path.Combine(path, pathOrFileName) 71 | 72 | let getFirstFile pattern (ExistingDirPath path) = 73 | let dirInfo = DirectoryInfo path 74 | dirInfo.GetFiles(pattern).[0] 75 | 76 | let value (ExistingDirPath path) = path 77 | 78 | let map f (ExistingDirPath path) = f path 79 | 80 | [] 81 | module ExistingFile = 82 | 83 | let wrap (file:FileInfo) = 84 | if not <| file.Exists then 85 | raise (new ArgumentException (sprintf "%s should've been an existing file, but wasn't." file.FullName)) 86 | else 87 | ExistingFile file 88 | 89 | let value (ExistingFile file) = file 90 | 91 | [] 92 | module ExistingFilePath = 93 | 94 | let create str = 95 | if not <| File.Exists(str) then 96 | raise (new ArgumentException (sprintf "%s should've led to an existing file, but didn't." str)) 97 | else 98 | ExistingFilePath str 99 | 100 | let getFileName (ExistingFilePath path) = Path.GetFileName(path) 101 | 102 | let getDirectoryName (ExistingFilePath path) = Path.GetDirectoryName(path) 103 | 104 | let value (ExistingFilePath path) = path 105 | 106 | let checkIfExisting path = path |> create |> value 107 | 108 | [] 109 | module RelativePath = 110 | 111 | let create (str:string) = 112 | if Path.IsPathRooted str then 113 | raise (new ArgumentException (sprintf "%s should've been a relative path, but wasn't." str)) 114 | else 115 | RelativePath str 116 | 117 | let isATopLevelPath (RelativePath path) = 118 | let trimmed = path.TrimStart('\\', '/') 119 | trimmed.Contains("\\") || 120 | trimmed.Contains("/") 121 | 122 | let isRoot (RelativePath path) = path.Length = 0 123 | 124 | let value (RelativePath path) = path -------------------------------------------------------------------------------- /src/Types.fs: -------------------------------------------------------------------------------- 1 | module Types 2 | 3 | open System.IO 4 | open UtilTypes 5 | open Newtonsoft.Json 6 | open JsonConverters 7 | 8 | type SingleProjectVsTemplate = 9 | { Xml: string } 10 | 11 | type ProjectTemplateLink = 12 | { VsTemplatePath: string 13 | SafeProjectName: string 14 | OriginalProjectName: string 15 | OriginalProjFilePath: string } 16 | 17 | type ProjectInfo = 18 | { ProjectFile: ExistingFile 19 | SolutionDestinationPath: RelativePath } 20 | 21 | type ProjectTemplateInfo = 22 | { VsTemplatePath: ExistingFilePath 23 | OriginalProjFilePath: ExistingFilePath 24 | OriginalProjectName: string 25 | SafeProjectName: string 26 | RootNamespace: string 27 | SolutionFolderDestinationPath: RelativePath } 28 | 29 | type MultiProjectVsTemplate = 30 | { Xml: string 31 | LinkElements: List 32 | InnerProjectsTemplateInfo: List } 33 | 34 | type MultiProjectTemplateInfo = 35 | { VsTemplatePath: ExistingFilePath 36 | RootDestinationPath: ExistingDirPath 37 | Projects: List } 38 | 39 | type MultiProjectTemplateArgs = 40 | { TemplateName: string 41 | TemplateDescription: string 42 | TemplateIcon: string 43 | TemplateWizardAssembly: string 44 | Destination: ExistingDirPath 45 | RootProjectNamespace: string 46 | FoldersToIgnore: List 47 | ProjectsDestinationInfo: List 48 | Projects: List } 49 | 50 | type ConvertProjectToTemplateArgs = 51 | { PathToSln: ExistingFilePath 52 | TemplateName: string 53 | TemplateDescription: string 54 | IconName: string 55 | TemplateWizardAssembly: string 56 | TemplateDestination: ExistingDirPath 57 | RootProjectNamespace: string 58 | FileExtensionsInExtraFoldersToIgnore: List 59 | FoldersToIgnore: List 60 | RootProjectPath: ExistingDirPath } 61 | 62 | type ExtraFolderMapping = 63 | { FolderPath: string 64 | ContentPath: string } 65 | 66 | type SolutionFolderNode = 67 | { FullPath: string 68 | Name: string 69 | Children: List } 70 | 71 | type SolutionFolderStructure = 72 | { Nodes: List } 73 | 74 | type ProjectDestinationInfoDto = 75 | { SafeProjectName: string 76 | ProjectName: string 77 | ProjectFileExtension: string 78 | DestinationDirectory: string 79 | DestinationSolutionDirectory: string } 80 | 81 | type StructureConfigurationDto = 82 | { Projects: List 83 | ExtraFolders: List 84 | SolutionFolderStructure: SolutionFolderStructure } 85 | 86 | type SingleProjectTemplateArgs = 87 | { ProjFilePath: ExistingFilePath 88 | PhysicalDestination: ExistingDirPath 89 | RootProjectNamespace: string 90 | FoldersToIgnore: List 91 | SolutionDestinationPath: string } 92 | 93 | type FolderNode = 94 | { Name: string 95 | Files: List 96 | FullPath: string 97 | ChildFolders: List } 98 | 99 | type PublishManifestIdentity = 100 | { InternalName: string 101 | Version: string 102 | DisplayName: string 103 | Description: string 104 | Tags: List } 105 | 106 | [)>] 107 | type VsixPriceCategory = 108 | | Free 109 | | Trial 110 | | Paid 111 | 112 | type GenerateTemplateWizardArgs = 113 | { WizardName: string 114 | TemplateZipPath: ExistingFilePath 115 | Destination: ExistingDirPath 116 | Version: string 117 | IconName: string 118 | PublisherFullName: string 119 | PublisherUsername: string 120 | DisplayName: string 121 | Description: string 122 | MoreInfo: string 123 | GettingStartedGuide: string 124 | InternalName: string 125 | Categories: List 126 | OverviewMdPath: string 127 | PriceCategory: VsixPriceCategory 128 | Qna: bool 129 | Repo: string option 130 | CustomPackageGuid: string option 131 | CustomProjectGuid: string option 132 | CustomId: string option 133 | Tags: String50 } 134 | 135 | type WizardPublishManifest = 136 | { Categories: List 137 | Identity: PublishManifestIdentity 138 | Overview: string 139 | PriceCategory: VsixPriceCategory 140 | Publisher: string 141 | Qna: bool 142 | [)>] 143 | Repo: string option } -------------------------------------------------------------------------------- /src/CLI.fs: -------------------------------------------------------------------------------- 1 | module CLI 2 | 3 | open Argu 4 | open Types 5 | 6 | type Arguments = 7 | | [] Template_Name of name:string 8 | | [] Template_Description of description:string 9 | | [] Template_Wizard_Assembly of assemblyName:string 10 | | [] Root_Project_Namespace of ``namespace``:string 11 | | [] Path_To_Sln of path:string 12 | | [] Vsix_Version of version:string 13 | | [] Vsix_Publisher_Full_Name of publisher:string 14 | | [] Vsix_Publisher_Username of publisher:string 15 | | [] Vsix_Display_Name of displayName:string 16 | | [] Vsix_Description of description:string 17 | | [] Vsix_More_Info of info:string 18 | | [] Vsix_Getting_Started of gettingStarted:string 19 | | [] Vsix_Overview_Md_Path of path:string 20 | | [] Vsix_Price_Category of VsixPriceCategory 21 | | [] Vsix_Qna_Enable of qnaEnabled:bool 22 | | [] Vsix_Internal_Name of name:string 23 | | [] Vsix_Repo of repo:string 24 | | [] Vsix_Custom_Icon of iconPath:string 25 | | [] Vsix_Custom_Package_Guid of guid:string 26 | | [] Vsix_Custom_Project_Guid of guid:string 27 | | [] Vsix_Custom_Id of id:string 28 | | [] Custom_Template_Icon of iconPath:string 29 | | [] Destination of destination:string 30 | | [] Vsix_Tags of tags: string 31 | | Vsix_Categories of categories:string list 32 | | Folders_To_Ignore of folderNames:string list 33 | | File_Extensions_To_Ignore of extensions: string list 34 | with 35 | interface IArgParserTemplate with 36 | member s.Usage = 37 | match s with 38 | | Template_Name _ -> "Sets the name of the template. This will be shown in Visual Studio." 39 | | Template_Description _ -> "Sets the description of the template. This will be shown in Visual Studio." 40 | | Template_Wizard_Assembly _ -> "Sets the name of the generated VSIX assembly." 41 | | Root_Project_Namespace _ -> "Sets the project root namespace (e.g. MyProject)." 42 | | Path_To_Sln _ -> "Sets the path to the source project .sln file." 43 | | Vsix_Version _ -> "Sets the version of the generated VSIX assembly." 44 | | Vsix_Publisher_Full_Name _ -> "The publisher's full name on the marketplace (e.g. John Smith)." 45 | | Vsix_Publisher_Username _ -> "The publisher's username on the marketplace (e.g. jsmith12)." 46 | | Vsix_Display_Name _ -> "Sets the display name of the generated VSIX assembly." 47 | | Vsix_Description _ -> "Sets the description of the generated VSIX assembly." 48 | | Vsix_More_Info _ -> "Sets the 'More Info' tag of the generated VSIX assembly." 49 | | Vsix_Getting_Started _ -> "Sets the 'Getting Started' tag of the generated VSIX assembly." 50 | | Vsix_Custom_Icon _ -> "Sets a custom icon for the VSIX. This appears in the Extensions tab." 51 | | Vsix_Custom_Package_Guid _ -> "Optionally sets a custom package GUID for the generated VSIX assembly." 52 | | Vsix_Custom_Project_Guid _ -> "Optionally sets a custom project GUID for the generated VSIX assembly." 53 | | Vsix_Custom_Id _ -> "Optionally sets a custom id for the generated VSIX assembly." 54 | | Custom_Template_Icon _ -> "Optionally sets a custom template icon." 55 | | Folders_To_Ignore _ -> "Which folders in the source project folder to ignore (e.g. bin, obj, lib, etc.)" 56 | | Destination _ -> "Sets a custom destination folder." 57 | | File_Extensions_To_Ignore _ -> "Which file extensions in the source folder to ignore (e.g. .min.js)" 58 | | Vsix_Categories _ -> "The categories for the VSIX in the marketplace." 59 | | Vsix_Repo _ -> "The repository url for the extension." 60 | | Vsix_Internal_Name _ -> "The extension's marketplace internal name." 61 | | Vsix_Qna_Enable _ -> "Whether or not your extension should have a Q&A section." 62 | | Vsix_Price_Category _ -> "The price category of the extension." 63 | | Vsix_Tags _ -> "The tags for the VSIX." 64 | | Vsix_Overview_Md_Path _ -> "The path to the overview.md file that will be shown in the marketplace." 65 | 66 | and InputOptionsArguments = 67 | | [] Inline of ParseResults 68 | | [] From_File of filePath:string 69 | with 70 | interface IArgParserTemplate with 71 | member s.Usage = 72 | match s with 73 | | Inline _ -> "Provide the arguments inline (as CLI arguments)." 74 | | From_File _ -> "Provide a .config file that holds the arguments." -------------------------------------------------------------------------------- /src/SingleProjectTemplateGenerator.fs: -------------------------------------------------------------------------------- 1 | module SingleProjectTemplateGenerator 2 | 3 | open Types 4 | open System.IO 5 | open Utils 6 | open UtilTypes 7 | 8 | let private copyProjectToTemplateDestination (projFilePath:ExistingFilePath) (templateDestination:ExistingDirPath) (foldersToIgnore:List) = 9 | let projectDirectory = ExistingFilePath.getDirectoryName projFilePath 10 | copyDirectory projectDirectory templateDestination true foldersToIgnore [] 11 | 12 | let private buildRelativeHierarchies destination foldersToIgnore = 13 | let hierarchies = buildHierarchies destination foldersToIgnore 14 | 15 | let destinationLength = 16 | destination 17 | |> ExistingDirPath.map (fun d -> d.Length) 18 | 19 | let cutPath (directory:FolderNode) = 20 | let newFullPath = 21 | directory.FullPath.Substring(destinationLength).Trim('\\', '/') 22 | 23 | { directory with FullPath = newFullPath } 24 | 25 | hierarchies 26 | |> Seq.map cutPath 27 | |> Seq.filter (fun n -> not <| n.FullPath.TrimStart('\\').Contains("\\")) 28 | 29 | let private replaceNamespacesAndUsingsInFile rootProjectNamespace (file:FileInfo) = 30 | let toReplacePairs = [ 31 | (sprintf "namespace %s" rootProjectNamespace, sprintf "namespace %s" Constants.ExtSafeProjectName); 32 | (sprintf "using %s" rootProjectNamespace, sprintf "using %s" Constants.ExtSafeProjectName); 33 | (rootProjectNamespace, Constants.ExtSafeProjectName) ] 34 | 35 | replaceInFile file.FullName toReplacePairs 36 | 37 | let rec private replaceNamespacesAndUsingsInNode rootProjectNamespace node = 38 | let replacedFiles = 39 | node.Files 40 | // TODO: Some more thought needs to be put into this 41 | // We don't want to be replacing phrases in *every* file because 42 | // we could accidently corrupt something 43 | |> List.filter (fun f -> f.Extension <> ".dll" && f.Extension <> ".zip") 44 | |> List.map (replaceNamespacesAndUsingsInFile rootProjectNamespace) 45 | 46 | let replacedFolders = 47 | node.ChildFolders 48 | |> List.map (replaceNamespacesAndUsingsInNode rootProjectNamespace) 49 | 50 | { node with 51 | Files = replacedFiles 52 | ChildFolders = replacedFolders } 53 | 54 | let private replaceNamespacesAndUsingsInHierarchy nodes rootProjectNamespace = 55 | nodes 56 | |> List.ofSeq 57 | |> List.map (replaceNamespacesAndUsingsInNode rootProjectNamespace) 58 | 59 | let private generateTemplateXml (templateXmlBuilder: string -> SingleProjectVsTemplate) projFilePath (destination:ExistingDirPath) rootProjectNamespace solutionFolderDestination = 60 | let projFileName = ExistingFilePath.getFileName projFilePath 61 | 62 | let template = templateXmlBuilder projFileName 63 | 64 | let projectName = projFileName |> cutEnd ".csproj" |> cutEnd ".fsproj" 65 | let templateFileName = sprintf "%s.vstemplate" projectName 66 | 67 | let destination = ExistingDirPath.value destination 68 | let templateDestination = 69 | Path.Combine(destination, templateFileName) 70 | 71 | File.WriteAllText(templateDestination, template.Xml) 72 | 73 | { VsTemplatePath = ExistingFilePath.create templateDestination 74 | OriginalProjFilePath = projFilePath 75 | OriginalProjectName = projectName 76 | // TODO: This is duplicated somewhere 77 | SafeProjectName = projectName.Replace(rootProjectNamespace, Constants.SafeProjectName) 78 | RootNamespace = rootProjectNamespace 79 | SolutionFolderDestinationPath = solutionFolderDestination } 80 | 81 | let private getTemplateXmlBuilder hierarchies = 82 | let rootNode = 83 | hierarchies 84 | |> Seq.find (fun h -> h.FullPath.Length = 0) 85 | 86 | // We need to differentiate between the files located 87 | // inside the root folder and those further down in the hierarchy 88 | // because there are differences when you generate the XML 89 | let rootProjectFiles = 90 | rootNode.Files 91 | |> Seq.map (fun f -> f.Name) 92 | // We won't include the original .csproj or .fsproj in the generated project 93 | |> Seq.filter (fun name -> not <| name.EndsWith("sproj")) 94 | |> List.ofSeq 95 | 96 | let hierarchies = 97 | hierarchies 98 | |> Seq.except [rootNode] 99 | |> List.ofSeq 100 | 101 | let templateXmlBuilder = 102 | SingleProjectVsTemplate.getTemplate 103 | rootProjectFiles 104 | hierarchies 105 | 106 | templateXmlBuilder 107 | 108 | /// 109 | /// Copies an existing project to a new destination and converts the project into a template. 110 | /// Also generates a .vstemplate file and writes it to the given destination. 111 | /// 112 | let generateTemplate args = 113 | copyProjectToTemplateDestination args.ProjFilePath args.PhysicalDestination args.FoldersToIgnore 114 | 115 | let hierarchies = 116 | buildRelativeHierarchies args.PhysicalDestination args.FoldersToIgnore 117 | |> List.ofSeq 118 | 119 | replaceNamespacesAndUsingsInHierarchy hierarchies args.RootProjectNamespace |> ignore 120 | 121 | let templateXmlBuilder = getTemplateXmlBuilder hierarchies 122 | 123 | generateTemplateXml 124 | templateXmlBuilder 125 | args.ProjFilePath 126 | args.PhysicalDestination 127 | args.RootProjectNamespace 128 | (RelativePath.create args.SolutionDestinationPath) -------------------------------------------------------------------------------- /tests/TestUtils.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module TestUtils 3 | 4 | open UtilTypes 5 | open System.Reflection 6 | open System.IO.Compression 7 | open System.IO 8 | open Utils 9 | open System 10 | open Argu 11 | open CLI 12 | open Xunit 13 | 14 | [] 15 | module Directories = 16 | let shouldContainFileWithPredicate' filePattern (filePredicate:FileInfo -> bool) (assertion:string -> bool -> Unit) (dir:DirectoryInfo) = 17 | dir.EnumerateFiles(filePattern) 18 | |> Seq.exists filePredicate 19 | |> assertion filePattern 20 | dir 21 | 22 | let shouldContainFileWithPredicate filePattern filePredicate dir = 23 | dir 24 | |> shouldContainFileWithPredicate' 25 | filePattern 26 | filePredicate 27 | (fun pattern -> fun result -> Assert.True(result, (sprintf "'%s' either did not contain a file with pattern '%s' or the file did not meet the predicate." dir.FullName pattern))) 28 | 29 | let shouldContainFile filePattern dir = 30 | shouldContainFileWithPredicate filePattern (fun _ -> true) dir 31 | 32 | let containsFoldersWithPattern pattern (dir:DirectoryInfo) = 33 | dir.EnumerateDirectories(pattern) 34 | |> Seq.isEmpty 35 | 36 | let containsFoldersWithPatterns patterns resultAssertion (dir:DirectoryInfo) = 37 | patterns 38 | |> List.iter (fun pattern -> 39 | dir 40 | |> containsFoldersWithPattern pattern 41 | |> resultAssertion pattern) 42 | dir 43 | 44 | let shouldContainFolders folderPatterns (dir:DirectoryInfo) = 45 | dir 46 | |> containsFoldersWithPatterns 47 | folderPatterns 48 | (fun pattern -> fun result -> Assert.False(result, (sprintf "'%s' should've contained a folder with pattern '%s', but it didn't." dir.FullName pattern))) 49 | 50 | let shouldNotContainFolders folderPatterns (dir:DirectoryInfo) = 51 | dir 52 | |> containsFoldersWithPatterns 53 | folderPatterns 54 | (fun pattern -> fun result -> Assert.True(result, (sprintf "'%s' shouldn't have contained a folder with pattern '%s', but it did." dir.FullName pattern))) 55 | 56 | [] 57 | module Setup = 58 | type SourceSlnFile = string 59 | type DestinationFolder = string 60 | type TemplateFolder = string 61 | type VsixFolder = string 62 | type CliArgs = ParseResults 63 | type ConstructArguments = SourceSlnFile -> DestinationFolder -> CliArgs 64 | type OnSuccessfulGenerationCallback = TemplateFolder -> VsixFolder -> CliArgs -> Unit 65 | 66 | let assemblyName = "SolutionSnapshotter.Tests" 67 | let tempFolderName = "ss-temp" 68 | let getShortGuid = Guid.NewGuid().ToString().Substring(0, 7) 69 | 70 | // If we use the assembly's execution location the path becomes too long 71 | // But the OS's temp folder is in another drive in Azure Pipelines so :) 72 | let getTempFolder = 73 | let agentBuildDir = Environment.GetEnvironmentVariable("agentBuildDir") 74 | 75 | if not <| String.IsNullOrEmpty(agentBuildDir) then 76 | Path.Combine(agentBuildDir, tempFolderName) 77 | else Path.Combine( 78 | Path.GetTempPath(), 79 | tempFolderName) 80 | 81 | let deleteIfExists dirName = 82 | if Directory.Exists dirName then 83 | Directory.Delete(dirName, true) 84 | else () 85 | 86 | let extractTestProjectSetup projectSetupName slnFileName : ExistingFilePath = 87 | use setupZipStream = 88 | Assembly 89 | .GetExecutingAssembly() 90 | .GetManifestResourceStream(sprintf "%s.TestProjects.%s.zip" assemblyName projectSetupName) 91 | 92 | if isNull setupZipStream then 93 | raise (InvalidOperationException (sprintf "Tried to execute a test with a non-existing setup. (setup name: %s)" projectSetupName)) 94 | 95 | use setupZipArchive = new ZipArchive(setupZipStream) 96 | 97 | let pathToExtractTo = 98 | Path.Combine( 99 | getTempFolder, 100 | // Use a unique folder each time so we can execute the tests in parallel 101 | getShortGuid, 102 | projectSetupName) 103 | 104 | setupZipArchive.ExtractToDirectory(pathToExtractTo) 105 | 106 | let slnFileName = 107 | slnFileName 108 | |> cutEnd ".sln" 109 | |> fun f -> f + ".sln" 110 | 111 | Path.Combine(pathToExtractTo, slnFileName) 112 | |> ExistingFilePath.create 113 | 114 | let generateSetupAndDo setupName slnFileName (constructArguments:ConstructArguments) (onSuccessfulGeneration:OnSuccessfulGenerationCallback) = 115 | let sourceSlnFile = extractTestProjectSetup setupName slnFileName |> ExistingFilePath.value 116 | 117 | let destinationFolder = 118 | Path.Combine( 119 | getTempFolder, 120 | (sprintf "dev-adv-%s" getShortGuid)) 121 | 122 | try 123 | let args = constructArguments sourceSlnFile destinationFolder 124 | let (_, templateZipPath, vsixCsprojPath) = Program.generateTemplate args 125 | onSuccessfulGeneration (Path.GetDirectoryName(templateZipPath |> ExistingFilePath.value)) (Path.GetDirectoryName(vsixCsprojPath)) args 126 | finally 127 | deleteIfExists (sourceSlnFile |> Path.GetDirectoryName |> Path.GetDirectoryName) 128 | deleteIfExists destinationFolder 129 | () 130 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ionide 2 | .ionide 3 | 4 | ## Vs code 5 | .vscode 6 | 7 | ## Ignore Visual Studio temporary files, build results, and 8 | ## files generated by popular Visual Studio add-ons. 9 | 10 | # User-specific files 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | bld/ 27 | [Bb]in/ 28 | [Oo]bj/ 29 | [Ll]og/ 30 | [Ll]ogs/ 31 | 32 | # Visual Studio 2015 cache/options directory 33 | .vs/ 34 | # Uncomment if you have tasks that create the project's static files in wwwroot 35 | #wwwroot/ 36 | 37 | # MSTest test Results 38 | [Tt]est[Rr]esult*/ 39 | [Bb]uild[Ll]og.* 40 | 41 | # NUNIT 42 | *.VisualState.xml 43 | TestResult.xml 44 | 45 | # Build Results of an ATL Project 46 | [Dd]ebugPS/ 47 | [Rr]eleasePS/ 48 | dlldata.c 49 | 50 | # DNX 51 | project.lock.json 52 | project.fragment.lock.json 53 | artifacts/ 54 | 55 | *_i.c 56 | *_p.c 57 | *_i.h 58 | *.ilk 59 | *.meta 60 | *.obj 61 | *.pch 62 | *.pdb 63 | *.pgc 64 | *.pgd 65 | *.rsp 66 | *.sbr 67 | *.tlb 68 | *.tli 69 | *.tlh 70 | *.tmp 71 | *.tmp_proj 72 | *.log 73 | *.vspscc 74 | *.vssscc 75 | .builds 76 | *.pidb 77 | *.svclog 78 | *.scc 79 | 80 | # Chutzpah Test files 81 | _Chutzpah* 82 | 83 | # Visual C++ cache files 84 | ipch/ 85 | *.aps 86 | *.ncb 87 | *.opendb 88 | *.opensdf 89 | *.sdf 90 | *.cachefile 91 | *.VC.db 92 | *.VC.VC.opendb 93 | 94 | # Visual Studio profiler 95 | *.psess 96 | *.vsp 97 | *.vspx 98 | *.sap 99 | 100 | # TFS 2012 Local Workspace 101 | $tf/ 102 | 103 | # Guidance Automation Toolkit 104 | *.gpState 105 | 106 | # ReSharper is a .NET coding add-in 107 | _ReSharper*/ 108 | *.[Rr]e[Ss]harper 109 | *.DotSettings.user 110 | 111 | # JustCode is a .NET coding add-in 112 | .JustCode 113 | 114 | # TeamCity is a build add-in 115 | _TeamCity* 116 | 117 | # DotCover is a Code Coverage Tool 118 | *.dotCover 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | #*.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignoreable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | node_modules/ 203 | orleans.codegen.cs 204 | 205 | # Since there are multiple workflows, uncomment next line to ignore bower_components 206 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 207 | #bower_components/ 208 | 209 | # RIA/Silverlight projects 210 | Generated_Code/ 211 | 212 | # Backup & report files from converting an old project file 213 | # to a newer Visual Studio version. Backup files are not needed, 214 | # because we have git ;-) 215 | _UpgradeReport_Files/ 216 | Backup*/ 217 | UpgradeLog*.XML 218 | UpgradeLog*.htm 219 | 220 | # SQL Server files 221 | *.mdf 222 | *.ldf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | 238 | # Visual Studio 6 build log 239 | *.plg 240 | 241 | # Visual Studio 6 workspace options file 242 | *.opt 243 | 244 | # Visual Studio LightSwitch build output 245 | **/*.HTMLClient/GeneratedArtifacts 246 | **/*.DesktopClient/GeneratedArtifacts 247 | **/*.DesktopClient/ModelManifest.xml 248 | **/*.Server/GeneratedArtifacts 249 | **/*.Server/ModelManifest.xml 250 | _Pvt_Extensions 251 | 252 | # Paket dependency manager 253 | .paket/paket.exe 254 | paket-files/ 255 | 256 | # FAKE - F# Make 257 | .fake/ 258 | 259 | # JetBrains Rider 260 | .idea/ 261 | *.sln.iml 262 | 263 | # CodeRush 264 | .cr/ 265 | 266 | # Python Tools for Visual Studio (PTVS) 267 | __pycache__/ 268 | *.pyc -------------------------------------------------------------------------------- /src/FodyWeavers.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks 13 | 14 | 15 | 16 | 17 | A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. 18 | 19 | 20 | 21 | 22 | A list of unmanaged 32 bit assembly names to include, delimited with line breaks. 23 | 24 | 25 | 26 | 27 | A list of unmanaged 64 bit assembly names to include, delimited with line breaks. 28 | 29 | 30 | 31 | 32 | The order of preloaded assemblies, delimited with line breaks. 33 | 34 | 35 | 36 | 37 | 38 | This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. 39 | 40 | 41 | 42 | 43 | Controls if .pdbs for reference assemblies are also embedded. 44 | 45 | 46 | 47 | 48 | Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. 49 | 50 | 51 | 52 | 53 | As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. 54 | 55 | 56 | 57 | 58 | Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. 59 | 60 | 61 | 62 | 63 | Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. 64 | 65 | 66 | 67 | 68 | A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | 69 | 70 | 71 | 72 | 73 | A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. 74 | 75 | 76 | 77 | 78 | A list of unmanaged 32 bit assembly names to include, delimited with |. 79 | 80 | 81 | 82 | 83 | A list of unmanaged 64 bit assembly names to include, delimited with |. 84 | 85 | 86 | 87 | 88 | The order of preloaded assemblies, delimited with |. 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. 97 | 98 | 99 | 100 | 101 | A comma-separated list of error codes that can be safely ignored in assembly verification. 102 | 103 | 104 | 105 | 106 | 'false' to turn off automatic generation of the XML Schema file. 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/Program.fs: -------------------------------------------------------------------------------- 1 | module Program 2 | 3 | open System.IO 4 | open Argu 5 | open CLI 6 | open Utils 7 | open UtilTypes 8 | open System 9 | 10 | let getDefaultDestinationPath = 11 | let defaultDestination = Path.Combine(@"C:\", Constants.DefaultDestinationFolderName) 12 | 13 | let generateDestinationFolder counter = 14 | sprintf "%s-%d" defaultDestination counter 15 | 16 | if not <| Directory.Exists defaultDestination then 17 | defaultDestination 18 | else 19 | let mutable counter = 1 20 | let mutable destination = generateDestinationFolder counter 21 | 22 | while Directory.Exists destination do 23 | counter <- counter + 1 24 | destination <- generateDestinationFolder counter 25 | 26 | destination 27 | 28 | let getDestinationPath customDestination = 29 | match customDestination with 30 | | Some path -> 31 | let path = 32 | path 33 | |> toRootedPath 34 | |> ExistingDirPath.createFromNonExistingOrFail 35 | |> ExistingDirPath.value 36 | 37 | path 38 | | None -> getDefaultDestinationPath 39 | 40 | let getArgs inputTypeArgs = 41 | let parser = ArgumentParser.Create(programName = Constants.ProgramName) 42 | 43 | match inputTypeArgs with 44 | | Inline args::_ -> 45 | args 46 | | From_File path::_ -> 47 | let configFilePath = 48 | toRootedPath path 49 | |> ExistingFilePath.checkIfExisting 50 | parser.ParseConfiguration(ConfigurationReader.FromAppSettingsFile(configFilePath)) 51 | | [] -> raise (ArgumentException "Invalid subcommand.") 52 | 53 | let generateTemplate (args:ParseResults) = 54 | let templateName = args.GetResult Template_Name 55 | let templateDescription = args.GetResult Template_Description 56 | let templateWizardAssembly = args.GetResult Template_Wizard_Assembly 57 | let rootProjectNamespace = args.GetResult Root_Project_Namespace 58 | let foldersToIgnore = args.GetResult Folders_To_Ignore 59 | let pathToSln = toRootedPath (args.GetResult Path_To_Sln) |> ExistingFilePath.create 60 | let rootProjectPath = ExistingFilePath.getDirectoryName pathToSln |> ExistingDirPath.create 61 | let rootTemplateDestination = getDestinationPath (args.TryGetResult Destination) 62 | let fileExtensionsToIgnore = args.GetResult File_Extensions_To_Ignore 63 | 64 | let templateDestination = 65 | Path.Combine(rootTemplateDestination, "template") 66 | |> ExistingDirPath.createFromNonExistingSafe 67 | 68 | let templateIconName = 69 | CustomFileHandler.setCustomFileOrEmbeddedDefault 70 | (args.TryGetResult Custom_Template_Icon) 71 | Constants.DefaultTemplateIcon 72 | templateDestination 73 | 74 | let templateZipPath = 75 | ProjectToTemplateConverter.convertProject 76 | { PathToSln = pathToSln 77 | TemplateName = templateName 78 | TemplateDescription = templateDescription 79 | IconName = templateIconName 80 | TemplateWizardAssembly = templateWizardAssembly 81 | TemplateDestination = templateDestination 82 | RootProjectNamespace = rootProjectNamespace 83 | FileExtensionsInExtraFoldersToIgnore = fileExtensionsToIgnore 84 | FoldersToIgnore = foldersToIgnore 85 | RootProjectPath = rootProjectPath } 86 | 87 | let vsixVersion = args.GetResult Vsix_Version 88 | let vsixPublisherFullName = args.GetResult Vsix_Publisher_Full_Name 89 | let vsixPublisherUsername = args.GetResult Vsix_Publisher_Username 90 | let vsixDisplayName = args.GetResult Vsix_Display_Name 91 | let vsixDescription = args.GetResult Vsix_Description 92 | let vsixMoreInfo = args.GetResult Vsix_More_Info 93 | let vsixGettingStartedGuide = args.GetResult Vsix_Getting_Started 94 | let vsixInternalName = args.GetResult Vsix_Internal_Name 95 | let vsixCategories = args.GetResult Vsix_Categories 96 | let vsixCustomPackageGuid = args.TryGetResult Vsix_Custom_Package_Guid 97 | let vsixCustomProjectGuid = args.TryGetResult Vsix_Custom_Project_Guid 98 | let vsixCustomId = args.TryGetResult Vsix_Custom_Id 99 | let vsixPriceCategory = args.GetResult Vsix_Price_Category 100 | let vsixQnaEnable = args.GetResult Vsix_Qna_Enable 101 | let vsixRepo = args.TryGetResult Vsix_Repo 102 | let vsixTags = args.TryGetResult Vsix_Tags |> Option.defaultValue "" |> String50.create 103 | 104 | let vsixProjectDestination = 105 | Path.Combine(rootTemplateDestination, "vsix") 106 | |> ExistingDirPath.createFromNonExistingSafe 107 | 108 | let vsixIconName = 109 | CustomFileHandler.setCustomFileOrEmbeddedDefault 110 | (args.TryGetResult Vsix_Custom_Icon) 111 | Constants.DefaultWizardIcon 112 | vsixProjectDestination 113 | 114 | let vsixOverviewMd = 115 | CustomFileHandler.setCustomFile 116 | (args.GetResult Vsix_Overview_Md_Path) 117 | vsixProjectDestination 118 | 119 | let vsixCsprojPath = 120 | TemplateWizardGenerator.generateWizard 121 | { WizardName = templateWizardAssembly 122 | TemplateZipPath = templateZipPath 123 | Destination = vsixProjectDestination 124 | IconName = vsixIconName 125 | Version = vsixVersion 126 | PublisherFullName = vsixPublisherFullName 127 | PublisherUsername = vsixPublisherUsername 128 | PriceCategory = vsixPriceCategory 129 | DisplayName = vsixDisplayName 130 | Qna = vsixQnaEnable 131 | Repo = vsixRepo 132 | Description = vsixDescription 133 | MoreInfo = vsixMoreInfo 134 | GettingStartedGuide = vsixGettingStartedGuide 135 | CustomPackageGuid = vsixCustomPackageGuid 136 | CustomProjectGuid = vsixCustomProjectGuid 137 | OverviewMdPath = vsixOverviewMd 138 | CustomId = vsixCustomId 139 | Categories = vsixCategories 140 | InternalName = vsixInternalName 141 | Tags = vsixTags } 142 | 143 | (pathToSln, templateZipPath, vsixCsprojPath) 144 | 145 | [] 146 | let main argv = 147 | try 148 | let parser = ArgumentParser.Create(programName = Constants.ProgramName) 149 | let inputType = parser.ParseCommandLine(inputs = argv, raiseOnUsage = true) 150 | let args = getArgs (inputType.GetAllResults()) 151 | let (pathToSln, templateZipPath, vsixCsprojPath) = generateTemplate args 152 | printfn "Successfully converted %s to a template!" (ExistingFilePath.getFileName pathToSln) 153 | printfn "You'll find the zipped template at '%s' and VSIX project at '%s'." (templateZipPath |> ExistingFilePath.value) vsixCsprojPath 154 | 0 155 | with e -> 156 | eprintfn "%s" e.Message 157 | 1 158 | -------------------------------------------------------------------------------- /src/TemplateWizardGenerator.fs: -------------------------------------------------------------------------------- 1 | module TemplateWizardGenerator 2 | 3 | open Types 4 | open Utils 5 | open System 6 | open System.Xml 7 | open System.IO 8 | open System.IO.Compression 9 | open System.Text.RegularExpressions 10 | open UtilTypes 11 | 12 | let vsixDefaultNamespace = "http://schemas.microsoft.com/developer/vsx-schema/2011" 13 | 14 | let private findVsixManifestNode nodeName (vsixManifestXml:XmlDocument) = 15 | vsixManifestXml.ChildNodes 16 | |> findNode nodeName 17 | 18 | let private getVsixManifestNode nodeName (vsixManifestXml:XmlDocument) = 19 | let node = 20 | vsixManifestXml.ChildNodes 21 | |> findNode nodeName 22 | 23 | match node with 24 | | Some n -> n 25 | | None -> 26 | raise 27 | (InvalidOperationException 28 | (sprintf "Expected to find node %s inside the template manifest, but it didn't exist. Most likely this means a broken WizardTemplate.zip." nodeName)) 29 | 30 | let private setVsixIdentityAttribute (attributeName:string) value (vsixManifest:XmlDocument) = 31 | let identityNode = vsixManifest |> getVsixManifestNode "Identity" 32 | identityNode.Attributes.[attributeName].Value <- value 33 | vsixManifest 34 | 35 | let private setVsixManifestMetadataNode nodeName value (vsixManifest:XmlDocument) = 36 | let node = vsixManifest |> findVsixManifestNode nodeName 37 | match node with 38 | | Some n -> n.InnerText <- value 39 | | None -> 40 | let metadataNode = vsixManifest |> getVsixManifestNode "Metadata" 41 | let newNode = createNode vsixManifest nodeName value vsixDefaultNamespace [] 42 | metadataNode.AppendChild(newNode) |> ignore 43 | () 44 | vsixManifest 45 | 46 | let private setGettingStartedGuide gettingStartedGuide vsixManifest = 47 | setVsixManifestMetadataNode "GettingStartedGuide" gettingStartedGuide vsixManifest 48 | 49 | let private setMoreInfo moreInfo vsixManifestFile = 50 | setVsixManifestMetadataNode "MoreInfo" moreInfo vsixManifestFile 51 | 52 | let private setDescription description vsixManifest = 53 | setVsixManifestMetadataNode "Description" description vsixManifest 54 | 55 | let private setDisplayName displayName vsixManifest = 56 | setVsixManifestMetadataNode "DisplayName" displayName vsixManifest 57 | 58 | let private setIcon iconName vsixManifest = 59 | setVsixManifestMetadataNode "Icon" iconName vsixManifest 60 | 61 | let private setTags tags vsixManifest = 62 | setVsixManifestMetadataNode "Tags" (tags |> String50.value) vsixManifest 63 | 64 | let private setPublisher publisher vsixManifest = 65 | setVsixIdentityAttribute "Publisher" publisher vsixManifest 66 | 67 | let private setVersion version vsixManifest = 68 | setVsixIdentityAttribute "Version" version vsixManifest 69 | 70 | let private setIdAttribute customId wizardName vsixManifest = 71 | setVsixIdentityAttribute 72 | "Id" 73 | (customId |> Option.defaultValue (sprintf "%s.%s" wizardName (Guid.NewGuid().ToString()))) 74 | vsixManifest 75 | 76 | let private getProjectFile (templateDirectory:DirectoryInfo) wizardName = 77 | templateDirectory.GetFiles(sprintf "%s.*sproj" wizardName).[0] 78 | 79 | let private replaceProjectGuid customProjectGuid wizardName (templateDirectory:DirectoryInfo) = 80 | let projFile = getProjectFile templateDirectory wizardName 81 | let contents = File.ReadAllText(projFile.FullName) 82 | 83 | let id = 84 | customProjectGuid 85 | |> Option.defaultValue (Guid.NewGuid().ToString()) 86 | 87 | contents.Replace("{$guid1$}", id) |> ignore 88 | templateDirectory 89 | 90 | let private replaceIconName iconName wizardName (templateDirectory:DirectoryInfo) = 91 | let projFile = (getProjectFile templateDirectory wizardName).FullName 92 | replaceInFile projFile [(Constants.VsixIconPlaceholder, iconName)] |> ignore 93 | templateDirectory 94 | 95 | let private replacePackageGuidString customPackageId wizardName (templateDirectory:DirectoryInfo) = 96 | let id = 97 | customPackageId 98 | |> Option.defaultValue (Guid.NewGuid().ToString().ToUpper()) 99 | 100 | let packageFile = templateDirectory.GetFiles(sprintf "%sPackage.cs" wizardName).[0] 101 | let contents = File.ReadAllText(packageFile.FullName) 102 | 103 | Regex.Replace( 104 | contents, 105 | "public const string PackageGuidString = \"(.*)\";", 106 | sprintf "public const string PackageGuidString = \"%s\";" id) 107 | |> ignore 108 | 109 | templateDirectory 110 | 111 | let private replaceUsings wizardName (tempZipTemplateDirectory:DirectoryInfo) = 112 | tempZipTemplateDirectory.EnumerateFiles("*", SearchOption.AllDirectories) 113 | |> Seq.map (fun f -> f.FullName) 114 | // We don't want to be changing text inside dlls 115 | |> Seq.filter (fun fPath -> not <| fPath.EndsWith(".dll")) 116 | |> Seq.map (fun filePath -> 117 | replaceInFile 118 | filePath 119 | [(Constants.SafeProjectName, wizardName) 120 | // Revert back if we've incidentally renamed a "$safeprojectname$" string literal 121 | // that is required in the code as-is 122 | (sprintf "\"%s\"" wizardName, sprintf "\"%s\"" Constants.SafeProjectName)] 123 | ) 124 | |> List.ofSeq 125 | |> ignore 126 | 127 | replaceInFileNames tempZipTemplateDirectory Constants.SafeProjectName wizardName |> ignore 128 | tempZipTemplateDirectory 129 | 130 | let private extractWizardZipToTempDirectory tempDestination = 131 | use wizardTemplateStream = getEmbeddedResourceStream Constants.WizardTemplateZip 132 | use wizardTemplateZip = new ZipArchive(wizardTemplateStream) 133 | wizardTemplateZip.ExtractToDirectory tempDestination 134 | DirectoryInfo tempDestination 135 | 136 | let private addTemplateToWizard templateZip (templateDirectory:ExistingDirPath) = 137 | let tempZipProjectTemplatesFolder = templateDirectory |> ExistingDirPath.combineWith Constants.ProjectTemplatesFolder 138 | Directory.CreateDirectory(tempZipProjectTemplatesFolder) |> ignore 139 | let templateZipPath = templateZip |> ExistingFilePath.value 140 | File.Copy(templateZipPath, Path.Combine(tempZipProjectTemplatesFolder, Constants.TemplateZip)) 141 | templateDirectory 142 | 143 | let private addPublishManifestFile (args:GenerateTemplateWizardArgs) templateDirectory = 144 | let tags = 145 | args.Tags 146 | |> String50.value 147 | |> fun s -> s.Split [|','|] 148 | |> Seq.map (fun s -> s.Trim(' ')) 149 | |> List.ofSeq 150 | 151 | let publishManifest = 152 | { Categories = args.Categories 153 | Overview = args.OverviewMdPath 154 | PriceCategory = args.PriceCategory 155 | Publisher = args.PublisherUsername 156 | Qna = args.Qna 157 | Repo = args.Repo 158 | Identity = 159 | { InternalName = args.InternalName 160 | Version = args.Version 161 | DisplayName = args.DisplayName 162 | Description = args.Description 163 | Tags = tags }} 164 | 165 | let publishManifestJson = PublishManifestGenerator.generatePublishManifestJson publishManifest 166 | let manifestJsonPath = templateDirectory |> ExistingDirPath.combineWith "publishManifest.json" 167 | File.WriteAllText(manifestJsonPath, publishManifestJson) 168 | 169 | templateDirectory 170 | 171 | let generateWizard (args:GenerateTemplateWizardArgs) = 172 | // We cannot use the OS's temp folder because it may be in a different drive 173 | let tempDestination = Path.Combine(Directory.GetCurrentDirectory(), Guid.NewGuid().ToString()) 174 | 175 | let generatedWizardDirectory = 176 | extractWizardZipToTempDirectory tempDestination 177 | |> replaceUsings args.WizardName 178 | |> replacePackageGuidString args.CustomPackageGuid args.WizardName 179 | |> replaceProjectGuid args.CustomProjectGuid args.WizardName 180 | |> replaceIconName args.IconName args.WizardName 181 | |> fun d -> d.FullName |> ExistingDirPath.create 182 | |> addTemplateToWizard args.TemplateZipPath 183 | |> addPublishManifestFile args 184 | |> moveFolderContents args.Destination 185 | 186 | let vsixManifestFile = 187 | generatedWizardDirectory 188 | |> ExistingDirPath.getFirstFile "*.vsixmanifest" 189 | 190 | let newVsixManifest = 191 | vsixManifestFile 192 | |> fun f -> File.ReadAllText(f.FullName) 193 | |> toXmlDocument 194 | |> setIdAttribute args.CustomId args.WizardName 195 | |> setIcon args.IconName 196 | |> setVersion args.Version 197 | |> setPublisher args.PublisherFullName 198 | |> setDisplayName args.DisplayName 199 | |> setDescription args.Description 200 | |> setMoreInfo args.MoreInfo 201 | |> setGettingStartedGuide args.GettingStartedGuide 202 | |> setTags args.Tags 203 | 204 | newVsixManifest.Save(vsixManifestFile.FullName) 205 | 206 | let vsixCsprojPath = args.Destination |> ExistingDirPath.combineWith (sprintf "%s.csproj" args.WizardName) 207 | vsixCsprojPath 208 | 209 | 210 | -------------------------------------------------------------------------------- /src/Utils.fs: -------------------------------------------------------------------------------- 1 | module Utils 2 | 3 | open System.IO 4 | open System.Xml 5 | open System.Linq 6 | open System.Reflection 7 | open System.Collections.Generic 8 | open System 9 | open Types 10 | 11 | [] 12 | module Collections = 13 | 14 | /// 15 | /// Reverses a seq. 16 | /// 17 | let reverse = System.Linq.Enumerable.Reverse 18 | 19 | [] 20 | module Directories = 21 | open UtilTypes 22 | 23 | let rec private getDirectoryParentHierarchyRec (directory:DirectoryInfo) (parentUpperBoundaryPath:string option) (hierarchy:List) = 24 | if directory = null || directory.Parent = null 25 | then [] 26 | else 27 | let directoryParent = directory.Parent 28 | 29 | if (parentUpperBoundaryPath.IsSome && directoryParent <> null && directoryParent.FullName = parentUpperBoundaryPath.Value) 30 | then [] 31 | else 32 | hierarchy.Add(directoryParent) 33 | // TODO: This may use some immutability 34 | ignore <| getDirectoryParentHierarchyRec directoryParent parentUpperBoundaryPath hierarchy 35 | List.ofSeq hierarchy 36 | 37 | let private getDirectoryParentHierarchy directory parentUpperBoundaryPath = 38 | List.ofSeq (getDirectoryParentHierarchyRec directory parentUpperBoundaryPath (new List<_>())) 39 | 40 | let private shouldBeIgnored foldersToIgnore (directory:DirectoryInfo) = 41 | if List.contains directory.Name foldersToIgnore 42 | then true 43 | else 44 | let parentHierarchy = getDirectoryParentHierarchy directory None 45 | 46 | parentHierarchy 47 | |> List.exists (fun d -> foldersToIgnore.Contains(d.Name)) 48 | 49 | let rec private getChildFolders (directory:DirectoryInfo) = 50 | seq { 51 | for childDirectory in directory.EnumerateDirectories() do 52 | // TODO: Duplication with the buildHierarchies function 53 | yield { 54 | FullPath = childDirectory.FullName 55 | ChildFolders = getChildFolders childDirectory |> List.ofSeq 56 | Files = childDirectory.GetFiles() |> List.ofSeq 57 | Name = childDirectory.Name 58 | } 59 | } 60 | 61 | let private buildHierarchiesFromDirs directories : FolderNode seq = 62 | seq { 63 | for (directory:DirectoryInfo) in directories do 64 | yield { 65 | FullPath = directory.FullName 66 | Files = directory.GetFiles() |> List.ofSeq 67 | ChildFolders = getChildFolders directory |> List.ofSeq 68 | Name = directory.Name 69 | } 70 | } 71 | 72 | let replaceInFile filePath phrasesToReplace = 73 | let mutable text = File.ReadAllText filePath 74 | 75 | List.iter (fun (toSearch:string, newText:string) -> 76 | text <- text.Replace(toSearch, newText) 77 | ) phrasesToReplace 78 | 79 | File.WriteAllText(filePath, text) 80 | FileInfo filePath 81 | 82 | 83 | let buildHierarchies (rootPath:ExistingDirPath) foldersToIgnore = 84 | let directory = 85 | ExistingDir.fromPath rootPath 86 | |> ExistingDir.value 87 | 88 | let subDirectories = 89 | directory.GetDirectories() 90 | |> Seq.filter (shouldBeIgnored foldersToIgnore >> not) 91 | |> List.ofSeq 92 | 93 | buildHierarchiesFromDirs ([directory] @ subDirectories) 94 | 95 | let scanForFiles rootPath (patterns:string list) = 96 | if (not <| Directory.Exists rootPath) then 97 | raise (new InvalidOperationException "Tried to scan an invalid directory.") 98 | 99 | let directory = DirectoryInfo rootPath 100 | 101 | patterns 102 | |> List.collect (fun pattern -> 103 | directory.EnumerateFiles(pattern, SearchOption.AllDirectories) 104 | |> List.ofSeq) 105 | 106 | let moveFolderContents (targetPath:ExistingDirPath) (source:ExistingDirPath) = 107 | let source = source |> ExistingDirPath.value 108 | let target = targetPath |> ExistingDirPath.value 109 | 110 | Directory.EnumerateFiles(source) 111 | |> Seq.iter (fun file -> 112 | let dest = Path.Combine(target, Path.GetFileName(file)) 113 | File.Move(file, dest)) 114 | 115 | Directory.EnumerateDirectories(source) 116 | |> Seq.iter (fun dir -> 117 | let dest = Path.Combine(target, Path.GetFileName(dir)) 118 | Directory.Move(dir, dest)) 119 | 120 | Directory.Delete(source) 121 | targetPath 122 | 123 | /// 124 | /// Scans the folder structure recursively. 125 | /// The bottom is a folder that contains a file with the specified pattern. 126 | /// Also scans the root folder you give it. 127 | /// 128 | let scanForDirectoriesThatDoNotContain rootPath patterns foldersToIgnore = 129 | if not <| Directory.Exists(rootPath) then 130 | raise (new InvalidOperationException "Tried to scan an invalid directory.") 131 | 132 | let directoriesToIgnore = 133 | scanForFiles rootPath patterns 134 | |> Seq.map (fun f -> f.DirectoryName) 135 | |> List.ofSeq 136 | 137 | if directoriesToIgnore |> List.contains rootPath then 138 | [] 139 | else 140 | 141 | let rootDirectoryInfo = DirectoryInfo rootPath 142 | 143 | let subdirectories = 144 | rootDirectoryInfo.EnumerateDirectories("*", SearchOption.AllDirectories) 145 | |> List.ofSeq 146 | 147 | (rootDirectoryInfo :: subdirectories) 148 | |> Seq.filter (fun directory -> 149 | // First we filter by the directory itself 150 | if shouldBeIgnored foldersToIgnore directory then 151 | false 152 | else 153 | // Then we check whether its parent hierarchy contains a folder that should be ignored 154 | let parentHierarchy = getDirectoryParentHierarchy directory (Some rootPath) 155 | 156 | let isOneOfIgnored = 157 | parentHierarchy 158 | |> List.exists (fun x -> directoriesToIgnore.Contains(x.FullName)) 159 | 160 | let containsUnwantedFiles (dir:DirectoryInfo) = 161 | patterns 162 | |> Seq.collect (fun p -> 163 | dir.GetFiles(p)) 164 | |> Seq.isEmpty 165 | |> not 166 | 167 | let shouldBeSelected = 168 | not <| isOneOfIgnored && 169 | not <| containsUnwantedFiles directory 170 | 171 | shouldBeSelected 172 | ) 173 | |> List.ofSeq 174 | 175 | /// 176 | /// Replaces the given phrases inside the file names of the given directory. 177 | /// Also includes files contained in subfolders. 178 | /// 179 | let replaceInFileNames (directory:DirectoryInfo) textToSearch newText = 180 | directory.EnumerateFiles(sprintf "*%s*.*" textToSearch, SearchOption.AllDirectories) 181 | |> Seq.map (fun file -> 182 | let destination = Path.Combine(file.DirectoryName, file.Name.Replace(textToSearch, newText)) 183 | file.MoveTo(destination) 184 | ) 185 | |> List.ofSeq 186 | 187 | /// 188 | /// Copies a directory's contents recursively. 189 | /// 190 | let rec copyDirectory sourceDirName (destDirName:ExistingDirPath) copySubDirs foldersToIgnore fileExtensionsToIgnore = 191 | let directoryInfo = DirectoryInfo sourceDirName 192 | 193 | if not <| directoryInfo.Exists then 194 | raise (DirectoryNotFoundException sourceDirName) 195 | 196 | let files = 197 | directoryInfo.EnumerateFiles() 198 | |> Seq.filter (fun f -> not <| List.contains f.Extension fileExtensionsToIgnore) 199 | 200 | Seq.iter (fun (file:FileInfo) -> 201 | let tempPath = destDirName |> ExistingDirPath.combineWith file.Name 202 | ignore <| file.CopyTo(tempPath, false) 203 | ) files 204 | 205 | if copySubDirs then 206 | let directories = 207 | directoryInfo.GetDirectories() 208 | |> Seq.filter (shouldBeIgnored foldersToIgnore >> not) 209 | 210 | Seq.iter (fun (subDirectory:DirectoryInfo) -> 211 | let tempPath = 212 | destDirName 213 | |> ExistingDirPath.combineWith subDirectory.Name 214 | |> ExistingDirPath.createFromNonExistingSafe 215 | 216 | copyDirectory subDirectory.FullName tempPath copySubDirs foldersToIgnore fileExtensionsToIgnore 217 | ) directories 218 | 219 | [] 220 | module String = 221 | 222 | /// 223 | /// If the string ends with the given piece, cuts it off. 224 | /// Else returns the string as it was. 225 | /// 226 | let cutEnd (toCut:string) (string:string) = 227 | if not <| string.EndsWith(toCut) then string 228 | else string.Substring(0, string.Length - toCut.Length) 229 | 230 | /// 231 | /// If the string starts with the given piece, cuts it off. 232 | /// Else returns the string as it was. 233 | /// 234 | let cutStart (toCut:string) (string:string) = 235 | if not <| string.StartsWith(toCut) then string 236 | else string.Substring(toCut.Length, string.Length - toCut.Length) 237 | 238 | /// 239 | /// Trims a given char off the end of a string. 240 | /// 241 | let trimEnd (chars:char list) (string:string) = 242 | string.TrimEnd(chars |> List.toArray) 243 | 244 | /// 245 | /// Trims a given char off the start of a string. 246 | /// 247 | let trimStart (char:char) (string:string) = 248 | string.TrimStart(char) 249 | 250 | /// 251 | /// Converts a string to a stream. 252 | /// 253 | let toStream (string: string) = 254 | let stream = new MemoryStream() 255 | let writer = new StreamWriter(stream) 256 | writer.Write(string) 257 | writer.Flush() 258 | stream.Position <- int64(0) 259 | stream 260 | 261 | [] 262 | module Xml = 263 | 264 | /// 265 | /// Searches for a node by name. 266 | /// 267 | let rec findNode name (nodes:XmlNodeList) : XmlNode option = 268 | let mutable nodeFound = None 269 | for node in nodes do 270 | if node.Name = name then nodeFound <- Some node 271 | elif node.HasChildNodes then 272 | let node = node.ChildNodes |> findNode name 273 | if node.IsSome then nodeFound <- node 274 | nodeFound 275 | 276 | /// 277 | /// Converts a string to System.Xml.XmlDocument. 278 | /// 279 | let toXmlDocument (string: string) = 280 | let document = XmlDocument() 281 | use stream = toStream string 282 | document.Load(stream) 283 | document 284 | 285 | /// 286 | /// Creates a node with the given parameters. 287 | /// Requires an xml document. 288 | /// 289 | let createNode (document:XmlDocument) name innerText xmlNamespace attributes = 290 | let node = document.CreateNode(XmlNodeType.Element, name, xmlNamespace) :?> XmlElement 291 | List.iter (fun (key, value) -> node.SetAttribute(key, value)) attributes 292 | node.InnerText <- innerText 293 | node 294 | 295 | /// 296 | /// Converts a relative path to a rooted. 297 | /// 298 | let toRootedPath (path:string) = 299 | if not <| Path.IsPathRooted path then 300 | Path.GetFullPath(path) 301 | else path 302 | 303 | /// 304 | /// Retrieves an embedded resource stream. 305 | /// 306 | let getEmbeddedResourceStream resourceName = 307 | let stream = 308 | Assembly 309 | .GetExecutingAssembly() 310 | .GetManifestResourceStream(sprintf "%s.%s" Constants.AssemblyName resourceName) 311 | 312 | if isNull stream then 313 | raise (InvalidOperationException (sprintf "Tried to retrieve an unexisting embedded resource with name '%s'" resourceName)) 314 | 315 | stream -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://dev.azure.com/dnikolovv/solution-snapshotter/_apis/build/status/dnikolovv.solution-snapshotter?branchName=master)](https://dev.azure.com/dnikolovv/solution-snapshotter/_build/latest?definitionId=4&branchName=master) 2 | 3 | # solution-snapshotter 4 | 5 | Take a snapshot of your current solution and instantly export it as a Visual Studio extension! 6 | 7 | ## Snapshot? 8 | 9 | In the context of this project, a snapshot means an exported Visual Studio template that represents your current project's state. 10 | 11 | If you have your project setup ready ([like this, for example](https://github.com/dnikolovv/devadventures-net-core-template/tree/master/source)), you can use this tool to export a ready-to-install Visual Studio extension instantly. The generated extension will contain a template that will initialize your project as you imported it. 12 | 13 | You can also read my [DotNetCurry article](https://www.dotnetcurry.com/visual-studio/1521/visual-studio-project-setup-solution-snapshotter) on why this tool was made and what problems does it solve. 14 | 15 | ## Table of contents 16 | 17 | 1. [What this tool does in pictures](#what-this-tool-does-in-pictures) 18 | 2. [Requirements](#requirements) 19 | 3. [Assumptions made](#assumption) 20 | 4. [An example project that uses this tool](#an-example-project-that-uses-it) 21 | 5. [What you can use it for](#what-you-could-use-it-for) 22 | 6. [Usage](#usage) 23 | 7. [Minimal usage](#minimal-usage) 24 | 8. [Contributing](#contributing) 25 | 26 | ## What this tool does in pictures 27 | 28 | Given a project setup that you've put together: 29 | 30 | ![1](example-pictures/step1.PNG) 31 | 32 | Call `solution-snapshotter.exe`: 33 | 34 | ![2](example-pictures/step2.PNG) 35 | ![3](example-pictures/step3.PNG) 36 | 37 | And receive a generated Visual Studio extension (VSIX) project: 38 | 39 | ![4](example-pictures/step4.PNG) 40 | 41 | ![5](example-pictures/step5.PNG) 42 | 43 | You can build it (or your CI agent): 44 | 45 | ![6](example-pictures/step6.png) 46 | 47 | And install it to Visual Studio (or ship it to the VS marketplace): 48 | 49 | ![7](example-pictures/step7.PNG) 50 | ![8](example-pictures/step8.PNG) 51 | 52 | After installing, your template will be available in Visual Studio under the name you've given it: 53 | 54 | ![9](example-pictures/step9.PNG) 55 | ![10](example-pictures/step10.PNG) 56 | 57 | The projects that you create will have the same physical and solution structure as your initial "source": 58 | 59 | ![11](example-pictures/step11.PNG) 60 | 61 | With any extra folders and files included (these could also be docker compose configuration files, for example): 62 | 63 | ![12](example-pictures/step12.PNG) 64 | 65 | And all references being valid (given that your source project was in a good state): 66 | 67 | ![13](example-pictures/step13.PNG) 68 | 69 | ## Requirements 70 | 71 | In order to build the generated VSIX (Visual Studio extension) project, you will need to have installed the `Visual Studio extension development` toolkit using the `Visual Studio Installer`. 72 | 73 | ![ext-dev](vs-ext-development.PNG) 74 | 75 | ## Assumption 76 | 77 | The tool makes one major assumption - your `.sln` file only references projects that are further down in the folder hierarchy. 78 | 79 | If it references project files in folders above in the hierarchy, don't expect it to work properly. Also, don't expect a proper template if you have your solution on the same level as the `.csproj` or `.fsproj` files. 80 | 81 | You may actually get a decent result, but these are not scenarios I've considered supporting. 82 | 83 | Example supported structures: 84 | 85 | ``` 86 | ├───client (some JS app, for example) 87 | ├───src 88 | | ├───MyProject.Api 89 | | ├───MyProject.Business 90 | | ├───MyProject.Core 91 | | ├───MyProject.Data 92 | └───MyProject.sln 93 | ``` 94 | 95 | ``` 96 | ├───MyProject.Api 97 | ├───MyProject.Business 98 | ├───MyProject.Core 99 | ├───MyProject.Data 100 | ├───MyProject.sln 101 | ``` 102 | 103 | An example unsupported structure: 104 | 105 | ``` 106 | ├───core 107 | | └───MyProject.Core (referenced by MyProject.sln) 108 | ├───project 109 | | ├───MyProject.sln 110 | │ ├───MyProject.Api 111 | │ ├───MyProject.Business 112 | │ ├───MyProject.Core 113 | │ └───MyProject.Data 114 | ``` 115 | 116 | ## An example project that uses it 117 | 118 | The [Dev Adventures .NET Core project setup](https://marketplace.visualstudio.com/items?itemName=dnikolovv.dev-adventures-project-setup&ssr=false#overview) is generated using this tool. 119 | 120 | The initial source is contained in [this GitHub repository](https://github.com/dnikolovv/devadventures-net-core-template/tree/master/source). 121 | 122 | On commits to `master`, the CI kicks in and uses `solution-snapshotter` to generate a `.vsix` file that is then automatically published to the VS Marketplace. You can see the extension page [here](https://marketplace.visualstudio.com/items?itemName=dnikolovv.dev-adventures-project-setup). 123 | 124 | ![https://devadventures.net/wp-content/uploads/2018/06/template-in-vs.png](https://devadventures.net/wp-content/uploads/2018/06/template-in-vs.png) 125 | 126 | ## What you could use it for 127 | 128 | If you work for a service company, use this tool to create and maintain standard templates for different project types. 129 | 130 | If you're doing microservices, use it to create common templates for services or shared libraries. 131 | 132 | Basically, every time you think a template would be handy. 133 | 134 | ## Usage 135 | 136 | `solution-snapshotter` supports two ways of providing arguments - inline or through a config file. 137 | 138 | ```console 139 | > solution-snapshotter.exe --help 140 | 141 | USAGE: solution-snapshotter.exe [--help] [from-file ] [ []] 142 | 143 | SUBCOMMANDS: 144 | 145 | inline Provide the arguments inline (as CLI arguments). 146 | 147 | Use 'solution-snapshotter.exe --help' for additional information. 148 | 149 | OPTIONS: 150 | 151 | from-file Provide a .config file that holds the arguments. 152 | --help display this list of options. 153 | ``` 154 | 155 | You can view the CLI documentation using the `--help` argument. Optional arguments are wrapped in square brackets. 156 | 157 | ```console 158 | > solution-snapshotter.exe inline --help 159 | 160 | USAGE: solution-snapshotter.exe inline [--help] --template-name --template-description --template-wizard-assembly --root-project-namespace --path-to-sln --vsix-version 161 | --vsix-publisher-full-name --vsix-publisher-username --vsix-display-name --vsix-description --vsix-more-info 162 | --vsix-getting-started --vsix-overview-md-path --vsix-price-category --vsix-qna-enable --vsix-internal-name [--vsix-repo=] 163 | [--vsix-custom-icon=] [--vsix-custom-package-guid=] [--vsix-custom-project-guid=] [--vsix-custom-id=] [--custom-template-icon=] [--destination=] 164 | [--vsix-tags=] [--vsix-categories [...]] [--folders-to-ignore [...]] [--file-extensions-to-ignore [...]] 165 | 166 | OPTIONS: 167 | 168 | --template-name 169 | Sets the name of the template. This will be shown in Visual Studio. 170 | --template-description 171 | Sets the description of the template. This will be shown in Visual Studio. 172 | --template-wizard-assembly 173 | Sets the name of the generated VSIX assembly. 174 | --root-project-namespace 175 | Sets the project root namespace (e.g. MyProject). 176 | --path-to-sln Sets the path to the source project .sln file. 177 | --vsix-version 178 | Sets the version of the generated VSIX assembly. 179 | --vsix-publisher-full-name 180 | The publisher's full name on the marketplace (e.g. John Smith). 181 | --vsix-publisher-username 182 | The publisher's username on the marketplace (e.g. jsmith12). 183 | --vsix-display-name 184 | Sets the display name of the generated VSIX assembly. 185 | --vsix-description 186 | Sets the description of the generated VSIX assembly. 187 | --vsix-more-info 188 | Sets the 'More Info' tag of the generated VSIX assembly. 189 | --vsix-getting-started 190 | Sets the 'Getting Started' tag of the generated VSIX assembly. 191 | --vsix-overview-md-path 192 | The path to the overview.md file that will be shown in the marketplace. 193 | --vsix-price-category 194 | The price category of the extension. 195 | --vsix-qna-enable 196 | Whether or not your extension should have a Q&A section. 197 | --vsix-internal-name 198 | The extension's marketplace internal name. 199 | --vsix-repo= The repository url for the extension. 200 | --vsix-custom-icon= 201 | Sets a custom icon for the VSIX. This appears in the Extensions tab. 202 | --vsix-custom-package-guid= 203 | Optionally sets a custom package GUID for the generated VSIX assembly. 204 | --vsix-custom-project-guid= 205 | Optionally sets a custom project GUID for the generated VSIX assembly. 206 | --vsix-custom-id= Optionally sets a custom id for the generated VSIX assembly. 207 | --custom-template-icon= 208 | Optionally sets a custom template icon. 209 | --destination= 210 | Sets a custom destination folder. 211 | --vsix-tags= The tags for the VSIX. 212 | --vsix-categories [...] 213 | The categories for the VSIX in the marketplace. 214 | --folders-to-ignore [...] 215 | Which folders in the source project folder to ignore (e.g. bin, obj, lib, etc.) 216 | --file-extensions-to-ignore [...] 217 | Which file extensions in the source folder to ignore (e.g. .min.js) 218 | --help display this list of options. 219 | ``` 220 | 221 | When supplying a `.config` file, use the same argument names, but instead of dashes, use spaces for the key values. 222 | 223 | Example: 224 | 225 | ```xml 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | ``` 256 | 257 | ```console 258 | > solution-snapshotter.exe from-file input.config 259 | 260 | Successfully converted .sln to a template! 261 | You'll find the zipped template at 'C:\some-path\Template.zip' and VSIX project at 'C:\some-path\SomeRandomWizard.csproj'. 262 | ``` 263 | 264 | ## Minimal usage 265 | 266 | If you want to get started quickly, simply download the latest **.exe** from the [releases page](https://github.com/dnikolovv/solution-snapshotter/releases), copy the following command and adjust the `path-to-sln`, `root-project-namespace` and `vsix-overview-md-path` parameters. The `overview.md` can be an empty **.md** file. Also, make sure to exclude the **.vs** folder. 267 | 268 | > Note: Don't expect to know what each parameter means if you're not familiar with the VSIX project type. Most of the parameters can and **will** be made optional in a future release. 269 | 270 | The `^` signs serve as newline characters in Windows's `cmd`. Remove them if you are not using it. 271 | 272 | ```console 273 | > solution-snapshotter.exe inline ^ 274 | --template-name MyAmazingSetup --template-description "It's truly amazing." ^ 275 | --path-to-sln "MyProject.sln" --root-project-namespace "MyProject" --folders-to-ignore "bin" ".vs" ^ 276 | --file-extensions-to-ignore ".csproj.user" --vsix-display-name "MyAmazingSetup Extension" ^ 277 | --vsix-description "MyAmazingSetup's description" --vsix-more-info "https://moreinfo.net" ^ 278 | --vsix-getting-started "https://gettingstarted.net" --vsix-price-category free --vsix-qna-enable true ^ 279 | --vsix-categories "other templates" --vsix-internal-name a-unique-internal-name ^ 280 | --vsix-overview-md-path "..\overview.md" --template-wizard-assembly "MyAmazingSetupWizard" ^ 281 | --vsix-version 1.0 --vsix-publisher-full-name "John Smith" --vsix-publisher-username "random-publisher" 282 | ``` 283 | 284 | You'll find your Visual Studio extension project under `C:\generated-template\vsix`. 285 | 286 | Note that to build the generated project you will need to have installed the `Visual Studio extension development` kit using the `Visual Studio Installer`. 287 | 288 | ![ext-dev](vs-ext-development.PNG) 289 | 290 | There are two ways to test your template. 291 | 292 | 1. Open the VSIX project inside Visual Studio and hit `Ctrl + F5` to open an experimental VS instance you can test in. 293 | 2. Build the project and install the generated `.vsix` file to your VS instance. 294 | 295 | ## Contributing 296 | 297 | Contributions are welcome. Also, don't hesitate to [open an issue](https://github.com/dnikolovv/solution-snapshotter/issues) if there's anything you think could be improved. 298 | 299 | ## License 300 | 301 | This project is licensed under the MIT License. 302 | --------------------------------------------------------------------------------