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:
Matt Lacey 2017-09-21 14:54:49 +01:00
Родитель e444fcdb5e
Коммит d3027de846
17 изменённых файлов: 225 добавлений и 12 удалений

Просмотреть файл

@ -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.