Wizard changes to enable adding Feedback Hub feature
For #196 + enables pages to be dependencies as well as features + enables chained dependencies + adds FileNameSearchPostAction to support merging with a file of unknown name + tests for tags starting "wts.fileNameSearch" in TemplateVerifier + docs updated for "wts.fileNameSearch[0-9]" + updated ApiAnalysis ref in TemplateVerifier to get needed bug fix
This commit is contained in:
Родитель
e444fcdb5e
Коммит
d3027de846
|
@ -181,6 +181,7 @@
|
|||
<Compile Include="Gen\GenContext.cs" />
|
||||
<Compile Include="Gen\GenToolBox.cs" />
|
||||
<Compile Include="Gen\IContextProvider.cs" />
|
||||
<Compile Include="PostActions\Catalog\FileNameSearchPostAction.cs" />
|
||||
<Compile Include="ProgrammingLanguages.cs" />
|
||||
<Compile Include="Naming\SuggestedDirectoryNameValidator.cs" />
|
||||
<Compile Include="PostActions\Catalog\Merge\FailedMergePostAction.cs" />
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.TemplateEngine.Abstractions;
|
||||
using Microsoft.Templates.Core.PostActions.Catalog.Merge;
|
||||
|
||||
namespace Microsoft.Templates.Core.PostActions.Catalog
|
||||
{
|
||||
public class FileNameSearchPostAction : PostAction<string>
|
||||
{
|
||||
public const string FileNameStart = "$SEARCH";
|
||||
public const string FileNamePattern = FileNameStart + "*.*";
|
||||
public const string SearchRegex = @"\$SEARCH([0-9]{1})\$";
|
||||
|
||||
public FileNameSearchPostAction(string config, ITemplateInfo template) : base(config)
|
||||
{
|
||||
var numeral = Regex.Match(Path.GetFileNameWithoutExtension(config), SearchRegex).Groups[1].Value;
|
||||
|
||||
SearchValue = template.GetFileNameSearch(numeral);
|
||||
}
|
||||
|
||||
private string SearchValue { get; }
|
||||
|
||||
public override void Execute()
|
||||
{
|
||||
var dirOfInterest = Path.GetDirectoryName(_config);
|
||||
|
||||
// Getting the extension this way and not using Path.GetExtension() as it treats ".cs" as the extension of "file.xaml.cs" when we need ".xaml.cs"
|
||||
var extOfInterest = Path.GetFileName(_config).Substring(Path.GetFileName(_config).IndexOf(".", StringComparison.Ordinal));
|
||||
|
||||
string targetFileName = null;
|
||||
|
||||
foreach (var file in new DirectoryInfo(dirOfInterest).GetFiles($"*{extOfInterest}"))
|
||||
{
|
||||
if (file.FullName != _config
|
||||
&& File.ReadAllText(file.FullName).Contains(SearchValue))
|
||||
{
|
||||
targetFileName = file.FullName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(targetFileName))
|
||||
{
|
||||
var replacement = Path.GetFileName(targetFileName).Replace(extOfInterest, string.Empty);
|
||||
|
||||
var newFileName = _config.Replace(Path.GetFileName(_config).Replace(extOfInterest, string.Empty), $"{replacement}_postaction");
|
||||
|
||||
File.Move(_config, newFileName);
|
||||
|
||||
if (extOfInterest == ".xaml.cs")
|
||||
{
|
||||
// Need to set the class name correctly for merge to work as it won't have been renamed like other files would
|
||||
var targetFileLines = File.ReadAllLines(targetFileName);
|
||||
|
||||
var classDefinitionLine = string.Empty;
|
||||
|
||||
foreach (var fileLine in targetFileLines)
|
||||
{
|
||||
if (fileLine.Contains(" class ") && !fileLine.Contains("//"))
|
||||
{
|
||||
classDefinitionLine = fileLine;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var mergefileLines = File.ReadAllLines(newFileName);
|
||||
|
||||
for (var i = 0; i < mergefileLines.Length; i++)
|
||||
{
|
||||
var mergefileLine = mergefileLines[i];
|
||||
if (mergefileLine.Contains(" class "))
|
||||
{
|
||||
mergefileLines[i] = classDefinitionLine;
|
||||
File.WriteAllLines(newFileName, mergefileLines);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mergeRenamedFileAction = new MergePostAction(new MergeConfiguration(newFileName, true));
|
||||
|
||||
mergeRenamedFileAction.Execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -75,7 +75,7 @@ namespace Microsoft.Templates.Core.PostActions.Catalog
|
|||
|
||||
private string GetFilePath()
|
||||
{
|
||||
if (Path.GetFileName(_config).StartsWith(Extension))
|
||||
if (Path.GetFileName(_config).StartsWith(Extension, StringComparison.Ordinal))
|
||||
{
|
||||
var extension = Path.GetExtension(_config);
|
||||
var directory = Path.GetDirectoryName(_config);
|
||||
|
|
|
@ -18,6 +18,7 @@ namespace Microsoft.Templates.Core.PostActions
|
|||
{
|
||||
var postActions = new List<PostAction>();
|
||||
|
||||
AddFileNameSearchActions(genInfo, postActions);
|
||||
AddGetMergeFilesFromProjectPostAction(postActions);
|
||||
AddGenerateMergeInfoPostAction(postActions);
|
||||
AddMergeActions(postActions, $"*{MergePostAction.Extension}*", false);
|
||||
|
|
|
@ -18,6 +18,7 @@ namespace Microsoft.Templates.Core.PostActions
|
|||
{
|
||||
var postActions = new List<PostAction>();
|
||||
|
||||
AddFileNameSearchActions(genInfo, postActions);
|
||||
AddPredefinedActions(genInfo, genResult, postActions);
|
||||
AddMergeActions(postActions, $"*{MergePostAction.Extension}*", true);
|
||||
AddSearchAndReplaceActions(postActions, $"*{SearchAndReplacePostAction.Extension}*");
|
||||
|
|
|
@ -74,6 +74,17 @@ namespace Microsoft.Templates.Core.PostActions
|
|||
.ForEach(f => postActions.Add(new MergePostAction(new MergeConfiguration(f, failOnError))));
|
||||
}
|
||||
|
||||
internal void AddFileNameSearchActions(GenInfo genInfo, List<PostAction> postActions)
|
||||
{
|
||||
if (genInfo.Template.HasAnyFileNameSearchTags())
|
||||
{
|
||||
Directory
|
||||
.EnumerateFiles(GenContext.Current.OutputPath, FileNameSearchPostAction.FileNamePattern, SearchOption.AllDirectories)
|
||||
.ToList()
|
||||
.ForEach(file => postActions.Add(new FileNameSearchPostAction(file, genInfo.Template)));
|
||||
}
|
||||
}
|
||||
|
||||
internal void AddGlobalMergeActions(List<PostAction> postActions, string searchPattern, bool failOnError)
|
||||
{
|
||||
Directory
|
||||
|
|
|
@ -299,6 +299,18 @@ namespace Microsoft.Templates.Core
|
|||
return result;
|
||||
}
|
||||
|
||||
public static bool HasAnyFileNameSearchTags(this ITemplateInfo ti)
|
||||
{
|
||||
return ti.Tags != null && ti.Tags.Any(t => t.Key.StartsWith(TagPrefix + "fileNameSearch", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
public static string GetFileNameSearch(this ITemplateInfo ti, string numeral)
|
||||
{
|
||||
var result = GetValueFromTag(ti, TagPrefix + "fileNameSearch" + numeral);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static bool GetMultipleInstance(this ITemplateInfo ti)
|
||||
{
|
||||
var result = GetValueFromTag(ti, TagPrefix + "multipleInstance");
|
||||
|
|
|
@ -82,13 +82,16 @@ namespace Microsoft.Templates.UI
|
|||
{
|
||||
LogOrAlertException(string.Format(StringRes.ExceptionDependencyMultipleInstance, dependencyTemplate.Identity));
|
||||
}
|
||||
else if (dependencyList.Any(d => d.Identity == template.Identity))
|
||||
else if (dependencyList.Any(d => d.Identity == template.Identity && d.GetDependencyList().Contains(template.Identity)))
|
||||
{
|
||||
LogOrAlertException(string.Format(StringRes.ExceptionDependencyCircularReference, template.Identity, dependencyTemplate.Identity));
|
||||
}
|
||||
else
|
||||
{
|
||||
dependencyList.Add(dependencyTemplate);
|
||||
if (!dependencyList.Contains(dependencyTemplate))
|
||||
{
|
||||
dependencyList.Add(dependencyTemplate);
|
||||
}
|
||||
|
||||
GetDependencies(dependencyTemplate, framework, dependencyList);
|
||||
}
|
||||
|
@ -177,9 +180,11 @@ namespace Microsoft.Templates.UI
|
|||
private static void AddDependencyTemplates((string name, ITemplateInfo template) selectionItem, List<GenInfo> genQueue, UserSelection userSelection)
|
||||
{
|
||||
var dependencies = GetAllDependencies(selectionItem.template, userSelection.Framework);
|
||||
|
||||
foreach (var dependencyItem in dependencies)
|
||||
{
|
||||
var dependencyTemplate = userSelection.Features.FirstOrDefault(f => f.template.Identity == dependencyItem.Identity);
|
||||
var dependencyTemplate = userSelection.PagesAndFeatures.FirstOrDefault(f => f.template.Identity == dependencyItem.Identity);
|
||||
|
||||
if (dependencyTemplate.template != null)
|
||||
{
|
||||
if (!genQueue.Any(t => t.Name == dependencyTemplate.name && t.Template.Identity == dependencyTemplate.template.Identity))
|
||||
|
|
|
@ -28,6 +28,22 @@ namespace Microsoft.Templates.UI
|
|||
public List<(string name, ITemplateInfo template)> Pages { get; } = new List<(string name, ITemplateInfo template)>();
|
||||
public List<(string name, ITemplateInfo template)> Features { get; } = new List<(string name, ITemplateInfo template)>();
|
||||
|
||||
public IEnumerable<(string name, ITemplateInfo template)> PagesAndFeatures
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var page in Pages)
|
||||
{
|
||||
yield return page;
|
||||
}
|
||||
|
||||
foreach (var feature in Features)
|
||||
{
|
||||
yield return feature;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
|
|
@ -20,7 +20,6 @@ namespace Microsoft.Templates.Test
|
|||
{
|
||||
// This is the relative path from where the test assembly will run from
|
||||
const string templatesRoot = "../../../../../Templates";
|
||||
// const string templatesRoot = "C:\\Users\\matt\\Documents\\GitHub\\WindowsTemplateStudio\\templates";
|
||||
|
||||
// The following excludes the catalog and project folders, but they only contain a single template file each
|
||||
var foldersOfInterest = new[] { "_composition", "Features", "Pages" };
|
||||
|
|
|
@ -7,6 +7,8 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Templates.Core;
|
||||
using Microsoft.Templates.Core.PostActions.Catalog;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace TemplateValidator
|
||||
|
@ -70,11 +72,26 @@ namespace TemplateValidator
|
|||
|
||||
var templateRoot = templateFilePath.Replace("\\.template.config\\template.json", string.Empty);
|
||||
|
||||
if (template.TemplateTags.Any(t => t.Key.StartsWith("wts.fileNameSearch", StringComparison.Ordinal)))
|
||||
{
|
||||
for (int i = 0; i <= 9; i++)
|
||||
{
|
||||
if (template.TemplateTags.ContainsKey($"wts.fileNameSearch{i}"))
|
||||
{
|
||||
if (!new DirectoryInfo(templateRoot).GetFiles($"{FileNameSearchPostAction.FileNameStart}{i}$.*", SearchOption.AllDirectories).Any())
|
||||
{
|
||||
results.Add($"'{templateFilePath}' contains the tag 'wts.fileNameSearch{i}' but has no corresponding $SEARCH{i}$ file.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var file in new DirectoryInfo(templateRoot).GetFiles("*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
// Filter out files the following tests cannot handle
|
||||
if (!file.Name.Contains("_postaction")
|
||||
&& !file.Name.Contains("_gpostaction")
|
||||
&& !file.Name.StartsWith(FileNameSearchPostAction.FileNameStart, StringComparison.Ordinal)
|
||||
&& !file.FullName.Contains("\\Projects\\Default")
|
||||
&& !file.FullName.Contains(".template.config"))
|
||||
{
|
||||
|
|
|
@ -208,10 +208,28 @@ namespace TemplateValidator
|
|||
case "wts.isHidden":
|
||||
VerifyWtsIshiddenTagValue(tag, results);
|
||||
break;
|
||||
default:
|
||||
if (tag.Key.StartsWith("wts.fileNameSearch", StringComparison.Ordinal))
|
||||
{
|
||||
VerifyWtsFileNameSearchTagValue(tag, results);
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add($"Unknown tag '{tag.Value}' specified in the file.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void VerifyWtsFileNameSearchTagValue(KeyValuePair<string, string> tag, List<string> results)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tag.Value))
|
||||
{
|
||||
results.Add($"The tag '{tag.Key}' cannot be blank if specified.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void VerifyWtsIshiddenTagValue(KeyValuePair<string, string> tag, List<string> results)
|
||||
{
|
||||
if (!BoolStrings.Contains(tag.Value))
|
||||
|
@ -370,7 +388,7 @@ namespace TemplateValidator
|
|||
bool.TryParse(template.TemplateTags["wts.multipleInstance"], out var allowMultipleInstances);
|
||||
if (!allowMultipleInstances)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(template.TemplateTags["wts.defaultInstance"]))
|
||||
if (!template.TemplateTags.Keys.Contains("wts.defaultInstance") || string.IsNullOrWhiteSpace(template.TemplateTags["wts.defaultInstance"]))
|
||||
{
|
||||
results.Add($"Template must define a valid value for wts.defaultInstance tag as wts.Type is '{tag.Value}' and wts.multipleInstance is 'false'.");
|
||||
}
|
||||
|
|
|
@ -46,8 +46,8 @@
|
|||
<RunCodeAnalysis>true</RunCodeAnalysis>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="ApiAnalysis.SimpleJsonAnalyzer, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\ApiAnalysis.SimpleJsonAnalyzer.1.3.0\lib\net462\ApiAnalysis.SimpleJsonAnalyzer.dll</HintPath>
|
||||
<Reference Include="ApiAnalysis.SimpleJsonAnalyzer, Version=1.4.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\ApiAnalysis.SimpleJsonAnalyzer.1.4.0\lib\net462\ApiAnalysis.SimpleJsonAnalyzer.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="CommandLine, Version=1.9.71.2, Culture=neutral, PublicKeyToken=de6f01bd326f8c32, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\CommandLineParser.1.9.71\lib\net45\CommandLine.dll</HintPath>
|
||||
|
@ -97,6 +97,7 @@
|
|||
<None Include="..\..\..\.editorconfig">
|
||||
<Link>.editorconfig</Link>
|
||||
</None>
|
||||
<None Include="app.config" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -110,4 +111,4 @@
|
|||
<Analyzer Include="..\..\packages\StyleCop.Analyzers.1.0.2\analyzers\dotnet\cs\StyleCop.Analyzers.dll" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
</Project>
|
|
@ -51,7 +51,7 @@ namespace TemplateValidator
|
|||
|
||||
// We just use strings for tags. The template engine uses a converter but this is fine for testing purposes
|
||||
[ApiAnalysisMandatoryKeys("language", "type", "wts.type")]
|
||||
[ApiAnalysisOptionalKeys("wts.displayOrder", "wts.compositionOrder", "wts.framework", "wts.projecttype", "wts.version", "wts.genGroup", "wts.rightClickEnabled", "wts.compositionFilter", "wts.licenses", "wts.group", "wts.multipleInstance", "wts.dependencies", "wts.defaultInstance", "wts.export.baseclass", "wts.export.setter", "wts.isHidden")]
|
||||
[ApiAnalysisOptionalKeys("wts.displayOrder", "wts.compositionOrder", "wts.framework", "wts.projecttype", "wts.version", "wts.genGroup", "wts.rightClickEnabled", "wts.compositionFilter", "wts.licenses", "wts.group", "wts.multipleInstance", "wts.dependencies", "wts.defaultInstance", "wts.export.baseclass", "wts.export.setter", "wts.isHidden", "wts.fileNameSearch1", "wts.fileNameSearch2", "wts.fileNameSearch3", "wts.fileNameSearch4", "wts.fileNameSearch5", "wts.fileNameSearch6", "wts.fileNameSearch7", "wts.fileNameSearch8", "wts.fileNameSearch9", "wts.fileNameSearch0")]
|
||||
[JsonProperty("tags")]
|
||||
public IReadOnlyDictionary<string, string> TemplateTags { get; set; }
|
||||
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.IO.Compression" publicKeyToken="b77a5c561934e089" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.1.2.0" newVersion="4.1.2.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Runtime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Runtime.Extensions" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-1.2.1.0" newVersion="1.2.1.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="ApiAnalysis.SimpleJsonAnalyzer" version="1.3.0" targetFramework="net462" />
|
||||
<package id="ApiAnalysis.SimpleJsonAnalyzer" version="1.4.0" targetFramework="net462" />
|
||||
<package id="CommandLineParser" version="1.9.71" targetFramework="net462" />
|
||||
<package id="Microsoft.TemplateEngine.Abstractions" version="1.0.0-beta2-20170518-234" targetFramework="net462" />
|
||||
<package id="Microsoft.TemplateEngine.Core" version="1.0.0-beta2-20170518-234" targetFramework="net462" />
|
||||
|
|
|
@ -357,7 +357,7 @@ We use the same strategy to integrate methods from Chart and Grid Page into the
|
|||
The format for global postactions is `<DestinationFileName>$<FeatureName>_gpostaction.<DestinationFileExtension>` (for example: BackgroundTaskService$BackgroundTaskFeature_gpostaction.cs).
|
||||
This allows generation of 1 gpostaction file per BackgroundTask selected and merge of all files once the generation has finished.
|
||||
|
||||
#### Merges Directives
|
||||
### Merges Directives
|
||||
|
||||
There are different merge directives to drive the code merging. Currently:
|
||||
|
||||
|
@ -368,6 +368,19 @@ There are different merge directives to drive the code merging. Currently:
|
|||
|
||||
_The above merge directives all use the C# comment form (`//`) but if included in a VB file should use the VB equivalent (`'`)_
|
||||
|
||||
### Merging with files of unknown names
|
||||
|
||||
Sometimes it may be necessary to merge with a file that has it's name defined by the user. For example, you may want to merge with the Settings page but this may have been renamed via the wizard.
|
||||
|
||||
To create a file that can be merged in this scenario requires two steps.
|
||||
|
||||
1. Create the file with the name "$SEARCH[0-9]$" and an extension that matches the file you wish to merge with. e.g. `$SEARCH1$.xaml` Put this file in the same directory as the one to merge with.
|
||||
1. Add a tag to the template config file with the name "wts.fileNameSearch[0-9]" (e.g. `"wts.fileNameSearch1"`) and a value that is some text within the file you wish to merge with. This search text should be unique to the file to merge with and should not contain anything dependent upon the name of the file.
|
||||
|
||||
Because it may be necessary for a template to include multiple files of unknown names that need to be merged, these can be specified by including a different number in the values above. The number in the file name will be matched with the tags containing the same number. e.g. `$SEARCH2$` will be matched with `wts.fileSearchName2`, etc.
|
||||
|
||||
Follow the above steps and the file will be merged with the one that contains the search text. An example of this functionality in use can be found in the FeedbackHub feature.
|
||||
|
||||
## Testing and verifying template contents
|
||||
|
||||
The tool **TemplateValidator.exe** exists to help template authors verify their templates are correctly structured and to identify common errors. It can verify the contents of an individual `template.json` file or the contents of multiple directories containing templates.
|
||||
|
|
Загрузка…
Ссылка в новой задаче