aspnetcore/src/Microsoft.AspNetCore.Razor..../DiscoverCommand.cs

223 lines
7.8 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.VisualStudio.LanguageServices.Razor;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.Razor.Tools
{
internal class DiscoverCommand : CommandBase
{
public DiscoverCommand(Application parent)
: base(parent, "discover")
{
Assemblies = Argument("assemblies", "assemblies to search for tag helpers", multipleValues: true);
TagHelperManifest = Option("-o", "output file", CommandOptionType.SingleValue);
ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue);
Version = Option("-v|--version", "Razor language version", CommandOptionType.SingleValue);
Configuration = Option("-c", "Razor configuration name", CommandOptionType.SingleValue);
ExtensionNames = Option("-n", "extension name", CommandOptionType.MultipleValue);
ExtensionFilePaths = Option("-e", "extension file path", CommandOptionType.MultipleValue);
}
public CommandArgument Assemblies { get; }
public CommandOption TagHelperManifest { get; }
public CommandOption ProjectDirectory { get; }
public CommandOption Version { get; }
public CommandOption Configuration { get; }
public CommandOption ExtensionNames { get; }
public CommandOption ExtensionFilePaths { get; }
protected override bool ValidateArguments()
{
if (string.IsNullOrEmpty(TagHelperManifest.Value()))
{
Error.WriteLine($"{TagHelperManifest.ValueName} must be specified.");
return false;
}
if (Assemblies.Values.Count == 0)
{
Error.WriteLine($"{Assemblies.Name} must have at least one value.");
return false;
}
if (string.IsNullOrEmpty(ProjectDirectory.Value()))
{
ProjectDirectory.Values.Add(Environment.CurrentDirectory);
}
if (string.IsNullOrEmpty(Version.Value()))
{
Error.WriteLine($"{Version.ValueName} must be specified.");
return false;
}
else if (!RazorLanguageVersion.TryParse(Version.Value(), out _))
{
Error.WriteLine($"{Version.ValueName} is not a valid language version.");
return false;
}
if (string.IsNullOrEmpty(Configuration.Value()))
{
Error.WriteLine($"{Configuration.ValueName} must be specified.");
return false;
}
if (ExtensionNames.Values.Count != ExtensionFilePaths.Values.Count)
{
Error.WriteLine($"{ExtensionNames.ValueName} and {ExtensionFilePaths.ValueName} should have the same number of values.");
}
foreach (var filePath in ExtensionFilePaths.Values)
{
if (!Path.IsPathRooted(filePath))
{
Error.WriteLine($"Extension file paths must be fully-qualified, absolute paths.");
return false;
}
}
if (!Parent.Checker.Check(ExtensionFilePaths.Values))
{
Error.WriteLine($"Extenions could not be loaded. See output for details.");
return false;
}
return true;
}
protected override Task<int> ExecuteCoreAsync()
{
// Loading all of the extensions should succeed as the dependency checker will have already
// loaded them.
var extensions = new RazorExtension[ExtensionNames.Values.Count];
for (var i = 0; i < ExtensionNames.Values.Count; i++)
{
extensions[i] = new AssemblyExtension(ExtensionNames.Values[i], Parent.Loader.LoadFromPath(ExtensionFilePaths.Values[i]));
}
var version = RazorLanguageVersion.Parse(Version.Value());
var configuration = new RazorConfiguration(version, Configuration.Value(), extensions);
var result = ExecuteCore(
configuration: configuration,
projectDirectory: ProjectDirectory.Value(),
outputFilePath: TagHelperManifest.Value(),
assemblies: Assemblies.Values.ToArray());
return Task.FromResult(result);
}
private int ExecuteCore(RazorConfiguration configuration, string projectDirectory, string outputFilePath, string[] assemblies)
{
outputFilePath = Path.Combine(projectDirectory, outputFilePath);
var metadataReferences = new MetadataReference[assemblies.Length];
for (var i = 0; i < assemblies.Length; i++)
{
metadataReferences[i] = Parent.AssemblyReferenceProvider(assemblies[i], default(MetadataReferenceProperties));
}
var engine = RazorProjectEngine.Create(configuration, RazorProjectFileSystem.Empty, b =>
{
b.Features.Add(new DefaultMetadataReferenceFeature() { References = metadataReferences });
b.Features.Add(new CompilationTagHelperFeature());
b.Features.Add(new DefaultTagHelperDescriptorProvider());
});
var feature = engine.Engine.Features.OfType<ITagHelperFeature>().Single();
var tagHelpers = feature.GetDescriptors();
using (var stream = new MemoryStream())
{
Serialize(stream, tagHelpers);
stream.Position = 0;
var newHash = Hash(stream);
var existingHash = Hash(outputFilePath);
if (!HashesEqual(newHash, existingHash))
{
stream.Position = 0;
using (var output = File.Open(outputFilePath, FileMode.Create))
{
stream.CopyTo(output);
}
}
}
return 0;
}
private static byte[] Hash(string path)
{
if (!File.Exists(path))
{
return Array.Empty<byte>();
}
using (var stream = File.OpenRead(path))
{
return Hash(stream);
}
}
private static byte[] Hash(Stream stream)
{
using (var sha = SHA256.Create())
{
sha.ComputeHash(stream);
return sha.Hash;
}
}
private bool HashesEqual(byte[] x, byte[] y)
{
if (x.Length != y.Length)
{
return false;
}
for (var i = 0; i < x.Length; i++)
{
if (x[i] != y[i])
{
return false;
}
}
return true;
}
private static void Serialize(Stream stream, IReadOnlyList<TagHelperDescriptor> tagHelpers)
{
using (var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: 4096, leaveOpen: true))
{
var serializer = new JsonSerializer();
serializer.Converters.Add(new TagHelperDescriptorJsonConverter());
serializer.Converters.Add(new RazorDiagnosticJsonConverter());
serializer.Serialize(writer, tagHelpers);
}
}
}
}