adding 'dotnet-scaffold' (#2727)
* added all 3 projects, need some cleaning up and unit tests * minor edits * not signing dotnet-scaffold anymore. * signing 3rd party spectre.console assemblies * fix WindowsEnvironmentVariableProvider and other minor nits. * more minor edits. * PR edits 1 * added 'required' modifiers to CommandInfo and Parameter, added some unit tests for their deserialization.
This commit is contained in:
Родитель
9e86dfb3c2
Коммит
34308bb481
63
All.sln
63
All.sln
|
@ -118,6 +118,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{CA1DAC
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Scaffolding.Shared.Tests", "test\Shared\Microsoft.DotNet.Scaffolding.Shared.Tests\Microsoft.DotNet.Scaffolding.Shared.Tests.csproj", "{A4C14F41-7EBC-40C2-93DF-B131B713AB8D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Scaffolding.Helpers", "src\Shared\Microsoft.DotNet.Scaffolding.Helpers\Microsoft.DotNet.Scaffolding.Helpers.csproj", "{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Scaffolding.ComponentModel", "src\Shared\Microsoft.DotNet.Scaffolding.ComponentModel\Microsoft.DotNet.Scaffolding.ComponentModel.csproj", "{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.Scaffolding.ComponentModel.Tests", "test\Shared\Microsoft.DotNet.Scaffolding.ComponentModel.Tests\Microsoft.DotNet.Scaffolding.ComponentModel.Tests.csproj", "{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
debug_x86|Any CPU = debug_x86|Any CPU
|
||||
|
@ -689,6 +695,60 @@ Global
|
|||
{A4C14F41-7EBC-40C2-93DF-B131B713AB8D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A4C14F41-7EBC-40C2-93DF-B131B713AB8D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A4C14F41-7EBC-40C2-93DF-B131B713AB8D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.debug_x86|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.debug_x86|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.debug_x86|x64.ActiveCfg = Debug|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.debug_x86|x64.Build.0 = Debug|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.debug_x86|x86.ActiveCfg = Debug|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.debug_x86|x86.Build.0 = Debug|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.debug_x86|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.debug_x86|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.debug_x86|x64.ActiveCfg = Debug|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.debug_x86|x64.Build.0 = Debug|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.debug_x86|x86.ActiveCfg = Debug|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.debug_x86|x86.Build.0 = Debug|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.debug_x86|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.debug_x86|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.debug_x86|x64.ActiveCfg = Debug|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.debug_x86|x64.Build.0 = Debug|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.debug_x86|x86.ActiveCfg = Debug|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.debug_x86|x86.Build.0 = Debug|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -732,6 +792,9 @@ Global
|
|||
{06E6C642-BB9B-4946-8AAA-8A8A463CBA48} = {A4057ACF-27F0-4724-963B-44548B6BC4E9}
|
||||
{CA1DAC77-5D18-4986-A4BB-F6171DEE5A52} = {81B64EBF-613D-43AE-82DA-B375FB751921}
|
||||
{A4C14F41-7EBC-40C2-93DF-B131B713AB8D} = {CA1DAC77-5D18-4986-A4BB-F6171DEE5A52}
|
||||
{E1F14286-FA19-47F2-8AB4-51B9DFA9EA27} = {06E6C642-BB9B-4946-8AAA-8A8A463CBA48}
|
||||
{A31A0E2D-F990-413C-91F6-8B5B8E570EC5} = {06E6C642-BB9B-4946-8AAA-8A8A463CBA48}
|
||||
{F85BABAD-4C0B-419C-AE6A-BA95D259AB53} = {CA1DAC77-5D18-4986-A4BB-F6171DEE5A52}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {26BCDDB3-5505-4903-9D87-C942ED0D03E6}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
<add key="dotnet-eng" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" />
|
||||
<add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" />
|
||||
<add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" />
|
||||
<add key="vs-impl" value="https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json" />
|
||||
<add key="dotnet9" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
|
||||
<add key="dotnet9-transport" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9-transport/nuget/v3/index.json" />
|
||||
<add key="dotnet8" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json" />
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
;; Exclusions for SignCheck. Corresponds to info in Signing.props.
|
||||
;; Format: https://github.com/dotnet/arcade/blob/397316e195639450b6c76bfeb9823b40bee72d6d/src/SignCheck/Microsoft.SignCheck/Verification/Exclusion.cs#L23-L35
|
||||
*.js;; We do not sign JavaScript files.
|
||||
*.js;; We do not sign JavaScript files.
|
|
@ -12,6 +12,8 @@
|
|||
<FileSignInfo Include = "Tavis.UriTemplates.dll" CertificateName="3PartySHA2"/>
|
||||
<FileSignInfo Include = "Mono.TextTemplating.dll" CertificateName="3PartySHA2"/>
|
||||
<FileSignInfo Include = "Std.UriTemplate.dll" CertificateName="3PartySHA2"/>
|
||||
<FileSignInfo Include = "Spectre.Console.Cli.dll" CertificateName="3PartySHA2"/>
|
||||
<FileSignInfo Include = "Spectre.Console.dll" CertificateName="3PartySHA2"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Code sign exclusions">
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<!-- Opt out of certain Arcade features -->
|
||||
<PropertyGroup>
|
||||
<UsingToolXliff>false</UsingToolXliff>
|
||||
<UsingToolNetFrameworkReferenceAssemblies>true</UsingToolNetFrameworkReferenceAssemblies>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<VersionPrefix>0.1.0</VersionPrefix>
|
||||
<PreReleaseVersionLabel>preview</PreReleaseVersionLabel>
|
||||
<IsServicingBuild Condition="'$(PreReleaseVersionLabel)' == 'servicing'">true</IsServicingBuild>
|
||||
<!--
|
||||
When StabilizePackageVersion is set to 'true', this branch will produce stable outputs for 'Shipping' packages
|
||||
-->
|
||||
<StabilizePackageVersion Condition="'$(StabilizePackageVersion)' == ''">false</StabilizePackageVersion>
|
||||
<DotNetFinalVersionKind Condition="'$(StabilizePackageVersion)' == 'true'">release</DotNetFinalVersionKind>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<SpectreConsoleFlowVersion>0.5.638</SpectreConsoleFlowVersion>
|
||||
<SpectreConsoleVersion>0.47.0</SpectreConsoleVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
|
@ -68,9 +68,9 @@
|
|||
<!-- Microsoft.AspNetCore.Razor.Language -->
|
||||
<MicrosoftAspNetCoreRazorLanguagePackageVersion>6.0.24</MicrosoftAspNetCoreRazorLanguagePackageVersion>
|
||||
<!-- Microsoft.Build-->
|
||||
<MicrosoftBuildPackageVersion>17.8.3 </MicrosoftBuildPackageVersion>
|
||||
<MicrosoftBuildPackageVersion>17.9.5 </MicrosoftBuildPackageVersion>
|
||||
<!-- Microsoft.Build.Utilities-->
|
||||
<MicrosoftBuildUtilitiesCorePackageVersion>17.8.3 </MicrosoftBuildUtilitiesCorePackageVersion>
|
||||
<MicrosoftBuildUtilitiesCorePackageVersion>17.9.5 </MicrosoftBuildUtilitiesCorePackageVersion>
|
||||
<MicrosoftBuildLocatorPackageVersion>1.7.1</MicrosoftBuildLocatorPackageVersion>
|
||||
<!-- Microsoft.CodeAnalysis.CSharp -->
|
||||
<MicrosoftCodeAnalysisCSharpPackageVersion>4.8.0</MicrosoftCodeAnalysisCSharpPackageVersion>
|
||||
|
@ -83,6 +83,7 @@
|
|||
<NewtonsoftJsonPackageVersion>13.0.3</NewtonsoftJsonPackageVersion>
|
||||
<NuGetPackagingVersion>6.9.1</NuGetPackagingVersion>
|
||||
<HumanizerPackageVersion>2.14.1</HumanizerPackageVersion>
|
||||
<MicrosoftVisualStudioSetupConfigurationInteropPackageVersion>3.9.2164</MicrosoftVisualStudioSetupConfigurationInteropPackageVersion>
|
||||
<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
|
||||
<!-- Everything below here are Packages only used by test projects -->
|
||||
<!-- Microsoft.AspNetCore.Server.Kestrel -->
|
||||
|
@ -94,7 +95,7 @@
|
|||
<!-- Microsoft.AspNetCore -->
|
||||
<!-- this is from aspnetcore-dev (test project only) -->
|
||||
<MicrosoftAspNetCorePackageVersion>6.0.0</MicrosoftAspNetCorePackageVersion>
|
||||
<MicrosoftBuildRuntimePackageVersion>17.8.3</MicrosoftBuildRuntimePackageVersion>
|
||||
<MicrosoftBuildRuntimePackageVersion>17.9.5</MicrosoftBuildRuntimePackageVersion>
|
||||
<!-- Microsoft.AspNetCore.Mvc -->
|
||||
<!-- this is from aspnetcore-dev (test only) -->
|
||||
<MicrosoftAspNetCoreMvcPackageVersion>6.0.0</MicrosoftAspNetCoreMvcPackageVersion>
|
||||
|
|
|
@ -1,22 +1,16 @@
|
|||
set VERSION=7.0.0-dev
|
||||
set VERSION=0.1.0-dev
|
||||
set DEFAULT_NUPKG_PATH=%userprofile%/.nuget/packages
|
||||
set SRC_DIR=%cd%
|
||||
set NUPKG=artifacts/packages/Debug/Shipping/
|
||||
call taskkill /f /im dotnet.exe
|
||||
call rd /Q /S artifacts
|
||||
call dotnet build Scaffolding.slnf
|
||||
call dotnet pack Scaffolding.slnf
|
||||
call build
|
||||
call dotnet tool uninstall -g Microsoft.dotnet-scaffold
|
||||
|
||||
call cd %DEFAULT_NUPKG_PATH%
|
||||
call rd /Q /S microsoft.visualstudio.web.codegeneration
|
||||
call rd /Q /S Microsoft.DotNet.Scaffolding.Shared
|
||||
call rd /Q /S microsoft.visualstudio.web.codegeneration.core
|
||||
call rd /Q /S microsoft.visualstudio.web.codegeneration.design
|
||||
call rd /Q /S microsoft.visualstudio.web.codegeneration.entityframeworkcore
|
||||
call rd /Q /S microsoft.visualstudio.web.codegeneration.templating
|
||||
call rd /Q /S microsoft.visualstudio.web.codegeneration.utils
|
||||
call rd /Q /S microsoft.visualstudio.web.codegenerators.mvc
|
||||
call rd /Q /S Microsoft.dotnet-scaffold
|
||||
call rd /Q /S Microsoft.DotNet.Scaffolding.Helpers
|
||||
call rd /Q /S Microsoft.DotNet.Scaffolding.ComponentModel
|
||||
|
||||
call cd %SRC_DIR%/%NUPKG%
|
||||
call dotnet tool install -g Microsoft.dotnet-scaffold --add-source %SRC_DIR%\%NUPKG% --version %VERSION%
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.ComponentModel;
|
||||
|
||||
/// <summary>
|
||||
/// What we want components to initialize and return (to stdout serialized as json) when 'get-commands' is invoked.
|
||||
/// Should be serialized as 'List<CommandInfo>'
|
||||
/// </summary>
|
||||
public class CommandInfo
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required string DisplayName { get; init; }
|
||||
public string? Description { get; set; }
|
||||
public required Parameter[] Parameters { get; init; }
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.ComponentModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Info from 'dotnet tool list -g'
|
||||
/// </summary>
|
||||
public class DotNetToolInfo
|
||||
{
|
||||
public string PackageName { get; set; } = default!;
|
||||
public string Version { get; set; } = default!;
|
||||
public string Command { get; set; } = default!;
|
||||
|
||||
public string ToDisplayString()
|
||||
{
|
||||
return $"{Command} ({PackageName} v{Version})";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)eng\Versions.Scaffold.props" />
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,56 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.ComponentModel;
|
||||
|
||||
public class Parameter
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required string DisplayName { get; init; }
|
||||
public required bool Required { get; set; } = false;
|
||||
public string? Description { get; set; }
|
||||
public required BaseTypes Type { get; set; } = BaseTypes.String;
|
||||
public InteractivePickerType? PickerType { get; set; }
|
||||
|
||||
internal static readonly ReadOnlyDictionary<BaseTypes, Type> TypeDict = new(
|
||||
new Dictionary<BaseTypes, Type>
|
||||
{
|
||||
{ BaseTypes.Bool, typeof(bool) },
|
||||
{ BaseTypes.Int, typeof(int) },
|
||||
{ BaseTypes.Long, typeof(long) },
|
||||
{ BaseTypes.Double, typeof(double) },
|
||||
{ BaseTypes.Decimal, typeof(decimal) },
|
||||
{ BaseTypes.Char, typeof(char) },
|
||||
{ BaseTypes.String, typeof(string) }
|
||||
});
|
||||
|
||||
public static Type GetParameterType(BaseTypes baseType)
|
||||
{
|
||||
return TypeDict[baseType];
|
||||
}
|
||||
|
||||
public Type GetParameterType()
|
||||
{
|
||||
return TypeDict[Type];
|
||||
}
|
||||
}
|
||||
|
||||
//will add List types for all these soon!
|
||||
public enum BaseTypes
|
||||
{
|
||||
Bool,
|
||||
Int,
|
||||
Long,
|
||||
Double,
|
||||
Decimal,
|
||||
Char,
|
||||
String
|
||||
}
|
||||
|
||||
public enum InteractivePickerType
|
||||
{
|
||||
ClassPicker,
|
||||
FilePicker,
|
||||
DbProviderPicker
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
namespace Microsoft.DotNet.Scaffolding.ComponentModel;
|
||||
|
||||
public static class ParameterHelpers
|
||||
{
|
||||
public static bool CheckType(BaseTypes baseType, string value)
|
||||
{
|
||||
var expectedType = Parameter.TypeDict[baseType];
|
||||
if (CanConvertToType(value, expectedType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool CanConvertToType(string value, Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
var converter = System.ComponentModel.TypeDescriptor.GetConverter(type);
|
||||
return converter.IsValid(value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Extensions.Roslyn
|
||||
{
|
||||
public static class ClassDeclarationSyntaxExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if class syntax node is based on type node with specified simple name
|
||||
/// (we cannot reliably compare full names for SyntaxNodes, we use symbols for full types.
|
||||
/// </summary>
|
||||
/// <param name="classNode"></param>
|
||||
/// <param name="baseTypeSimpleName"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsBasedOn(this ClassDeclarationSyntax classNode, string baseTypeSimpleName)
|
||||
{
|
||||
if (classNode is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return classNode.BaseList?.Types.Any(
|
||||
x => string.Equals(x.Type.ToString(), baseTypeSimpleName, StringComparison.Ordinal)) == true;
|
||||
}
|
||||
|
||||
public static bool IsStaticClass(this ClassDeclarationSyntax? classDeclaration)
|
||||
{
|
||||
return classDeclaration is not null ?
|
||||
classDeclaration.Modifiers.Any(x => x.Text.Equals(SyntaxFactory.Token(SyntaxKind.StaticKeyword).Text)) :
|
||||
false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Extensions.Roslyn
|
||||
{
|
||||
public static class RoslynExtensions
|
||||
{
|
||||
public static Project? GetProject(this Solution? solution, string? projectPath)
|
||||
{
|
||||
return solution?.Projects?.FirstOrDefault(x => string.Equals(projectPath, x.FilePath, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public static Document? GetDocument(this Project project, string? documentName)
|
||||
{
|
||||
var fileName = Path.GetFileName(documentName);
|
||||
if (string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
//often Document.Name is the file path of the document and not the name.
|
||||
//check for all possible cases.
|
||||
return project.Documents.FirstOrDefault(x =>
|
||||
x.Name.EndsWith(fileName, StringComparison.OrdinalIgnoreCase) ||
|
||||
x.Name.Equals(documentName, StringComparison.OrdinalIgnoreCase) ||
|
||||
(!string.IsNullOrEmpty(x.FilePath) &&
|
||||
x.FilePath.Equals(documentName, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Extensions
|
||||
{
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static bool IsCSharpProject(this string projectFilePath)
|
||||
{
|
||||
return projectFilePath?.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) == true;
|
||||
}
|
||||
|
||||
public static bool IsBinary(this string filePath)
|
||||
{
|
||||
return
|
||||
filePath?.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) == true ||
|
||||
filePath?.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) == true;
|
||||
}
|
||||
|
||||
public static bool IsSolutionFile(this string filePath)
|
||||
{
|
||||
return
|
||||
filePath?.EndsWith(".sln", StringComparison.OrdinalIgnoreCase) == true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// since netstandard2.0 does not have a Contains that allows StringOrdinal param, using lower invariant comparison.
|
||||
/// </summary>
|
||||
/// <returns>true if lower invariants are equal, false otherwise. false for any null scenario.</returns>
|
||||
public static bool ContainsIgnoreCase(this string input, string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (input?.ToLowerInvariant().Contains(value.ToLowerInvariant())).GetValueOrDefault();
|
||||
}
|
||||
|
||||
public static string ToLowerInvariantFirstChar(this string input)
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(input));
|
||||
}
|
||||
|
||||
if (input != string.Empty)
|
||||
{
|
||||
return input.Substring(0, length: 1).ToLowerInvariant() + input.Substring(1);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to make relative paths from physical paths. ie. calculates the relative path from
|
||||
/// basePath to fullPath. Note that if either path is null, fullPath will be returned.
|
||||
/// Note that two paths that are equal return ".\".
|
||||
/// </summary>
|
||||
public static string MakeRelativePath(this string fullPath, string basePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(basePath) || string.IsNullOrEmpty(fullPath))
|
||||
{
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
var separator = Path.DirectorySeparatorChar.ToString();
|
||||
var tempBasePath = basePath.EnsureTrailingBackslash();
|
||||
var tempFullPath = fullPath.EnsureTrailingBackslash();
|
||||
|
||||
string relativePath = string.Empty;
|
||||
|
||||
while (!string.IsNullOrEmpty(tempBasePath))
|
||||
{
|
||||
if (tempFullPath.StartsWith(tempBasePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Since we may have added the trailing slash we have to account for that here
|
||||
if (fullPath.Length < tempBasePath.Length)
|
||||
{
|
||||
Debug.Assert(
|
||||
tempBasePath.Length - fullPath.Length == 1,
|
||||
"We are at the end. Nothing more to do. Add an empty string to handle case where the paths are equal");
|
||||
}
|
||||
else
|
||||
{
|
||||
relativePath += fullPath.Remove(0, tempBasePath.Length);
|
||||
}
|
||||
|
||||
// Two equal paths are relative by .\
|
||||
if (string.IsNullOrEmpty(relativePath))
|
||||
{
|
||||
relativePath = "." + Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
tempBasePath = tempBasePath.Remove(tempBasePath.Length - 1);
|
||||
var nLastIndex = tempBasePath.LastIndexOf(separator, StringComparison.OrdinalIgnoreCase);
|
||||
if (-1 != nLastIndex)
|
||||
{
|
||||
tempBasePath = tempBasePath.Remove(nLastIndex + 1);
|
||||
relativePath += "..";
|
||||
relativePath += separator;
|
||||
}
|
||||
else
|
||||
{
|
||||
relativePath = fullPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes sure the string has a trailing backslash
|
||||
/// </summary>
|
||||
public static string EnsureTrailingBackslash(this string s)
|
||||
{
|
||||
return s.EnsureTrailingChar(Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes sure the string has the trailing character
|
||||
/// </summary>
|
||||
public static string EnsureTrailingChar(this string s, char ch)
|
||||
{
|
||||
return s.Length == 0 || s[s.Length - 1] != ch ? s + ch : s;
|
||||
}
|
||||
|
||||
public static string WithOsPathSeparators(this string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.Replace(IncorrectPathSeparator, Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
private static readonly char IncorrectPathSeparator = Path.DirectorySeparatorChar == '\\' ? '/' : '\\';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers
|
||||
{
|
||||
public static class ArgumentEscaper
|
||||
{
|
||||
/// <summary>
|
||||
/// Undo the processing which took place to create string[] args in Main,
|
||||
/// so that the next process will receive the same string[] args
|
||||
///
|
||||
/// See here for more info:
|
||||
/// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
|
||||
/// </summary>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
public static string EscapeAndConcatenateArgArrayForProcessStart(IEnumerable<string> args)
|
||||
{
|
||||
return string.Join(" ", EscapeArgArray(args));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Undo the processing which took place to create string[] args in Main,
|
||||
/// so that the next process will receive the same string[] args
|
||||
///
|
||||
/// See here for more info:
|
||||
/// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
|
||||
/// </summary>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
private static IEnumerable<string> EscapeArgArray(IEnumerable<string> args)
|
||||
{
|
||||
var escapedArgs = new List<string>();
|
||||
|
||||
foreach (var arg in args)
|
||||
{
|
||||
escapedArgs.Add(EscapeSingleArg(arg));
|
||||
}
|
||||
|
||||
return escapedArgs;
|
||||
}
|
||||
|
||||
public static string EscapeSingleArg(string arg)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var needsQuotes = ShouldSurroundWithQuotes(arg);
|
||||
var isQuoted = needsQuotes || IsSurroundedWithQuotes(arg);
|
||||
|
||||
if (needsQuotes) sb.Append("\"");
|
||||
|
||||
for (int i = 0; i < arg.Length; ++i)
|
||||
{
|
||||
var backslashCount = 0;
|
||||
|
||||
// Consume All Backslashes
|
||||
while (i < arg.Length && arg[i] == '\\')
|
||||
{
|
||||
backslashCount++;
|
||||
i++;
|
||||
}
|
||||
|
||||
// Escape any backslashes at the end of the arg
|
||||
// when the argument is also quoted.
|
||||
// This ensures the outside quote is interpreted as
|
||||
// an argument delimiter
|
||||
if (i == arg.Length && isQuoted)
|
||||
{
|
||||
sb.Append('\\', 2 * backslashCount);
|
||||
}
|
||||
|
||||
// At then end of the arg, which isn't quoted,
|
||||
// just add the backslashes, no need to escape
|
||||
else if (i == arg.Length)
|
||||
{
|
||||
sb.Append('\\', backslashCount);
|
||||
}
|
||||
|
||||
// Escape any preceding backslashes and the quote
|
||||
else if (arg[i] == '"')
|
||||
{
|
||||
sb.Append('\\', (2 * backslashCount) + 1);
|
||||
sb.Append('"');
|
||||
}
|
||||
|
||||
// Output any consumed backslashes and the character
|
||||
else
|
||||
{
|
||||
sb.Append('\\', backslashCount);
|
||||
sb.Append(arg[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (needsQuotes) sb.Append("\"");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
internal static bool ShouldSurroundWithQuotes(string argument)
|
||||
{
|
||||
// Don't quote already quoted strings
|
||||
if (IsSurroundedWithQuotes(argument))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only quote if whitespace exists in the string
|
||||
return ArgumentContainsWhitespace(argument);
|
||||
}
|
||||
|
||||
internal static bool IsSurroundedWithQuotes(string argument)
|
||||
{
|
||||
return argument.StartsWith("\"", StringComparison.Ordinal) &&
|
||||
argument.EndsWith("\"", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
internal static bool ArgumentContainsWhitespace(string argument)
|
||||
{
|
||||
return argument.Contains(" ") || argument.Contains("\t") || argument.Contains("\n");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.General;
|
||||
|
||||
public static class CliHelpers
|
||||
{
|
||||
public static IDictionary<string, List<string>> ParseILookup(ILookup<string, string?> lookup)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(lookup);
|
||||
var parsedDict = new Dictionary<string, List<string>>();
|
||||
foreach (var group in lookup)
|
||||
{
|
||||
var valList = new List<string>();
|
||||
var key = group.Key;
|
||||
foreach (var element in group)
|
||||
{
|
||||
if (element != null)
|
||||
{
|
||||
valList.Add(element);
|
||||
}
|
||||
}
|
||||
|
||||
parsedDict.Add(key, valList);
|
||||
}
|
||||
|
||||
return parsedDict;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.General;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public static class EnvironmentVariables
|
||||
{
|
||||
public const string MSBuildExtensionsPath32 = nameof(MSBuildExtensionsPath32);
|
||||
public const string MSBuildExtensionsPath = nameof(MSBuildExtensionsPath);
|
||||
public const string MSBUILD_EXE_PATH = nameof(MSBUILD_EXE_PATH);
|
||||
public const string MSBuildSDKsPath = nameof(MSBuildSDKsPath);
|
||||
public const string USERPROFILE = nameof(USERPROFILE);
|
||||
public const string VSINSTALLDIR = nameof(VSINSTALLDIR);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.General;
|
||||
|
||||
public static class DotNetMuxer
|
||||
{
|
||||
private const string MuxerName = "dotnet";
|
||||
|
||||
static DotNetMuxer()
|
||||
{
|
||||
MuxerPath = TryFindMuxerPath();
|
||||
}
|
||||
|
||||
public static string MuxerPath { get; }
|
||||
|
||||
public static string MuxerPathOrDefault()
|
||||
=> MuxerPath ?? MuxerName;
|
||||
|
||||
private static string TryFindMuxerPath()
|
||||
{
|
||||
var fileName = MuxerName;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
fileName += ".exe";
|
||||
}
|
||||
|
||||
var mainModule = Process.GetCurrentProcess().MainModule;
|
||||
if (!string.IsNullOrEmpty(mainModule?.FileName)
|
||||
&& Path.GetFileName(mainModule.FileName).Equals(fileName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return mainModule.FileName;
|
||||
}
|
||||
|
||||
//TODO; fix to have a nullable Command and not resure scaffolding's. Tracked https://github.com/dotnet/Scaffolding/issues/1549
|
||||
#pragma warning disable CS8603 // Possible null reference return.
|
||||
return null;
|
||||
#pragma warning restore CS8603 // Possible null reference return.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.General;
|
||||
|
||||
/// <summary>
|
||||
/// To run 'dotnet' or any processes and capture consequent stdout and stderr (using 'ExecuteAndCaptureOutput'
|
||||
/// </summary>
|
||||
public class DotnetCliRunner
|
||||
{
|
||||
public static DotnetCliRunner CreateDotNet(string commandName, IEnumerable<string> args)
|
||||
{
|
||||
return Create(DotNetMuxer.MuxerPathOrDefault(), new[] { commandName }.Concat(args));
|
||||
}
|
||||
|
||||
public static DotnetCliRunner Create(string commandName, IEnumerable<string> args)
|
||||
{
|
||||
return new DotnetCliRunner(commandName, args);
|
||||
}
|
||||
|
||||
public int ExecuteAndCaptureOutput(out string? stdOut, out string? stdErr)
|
||||
{
|
||||
using var outStream = new ProcessOutputStreamReader();
|
||||
using var errStream = new ProcessOutputStreamReader();
|
||||
|
||||
outStream.Capture();
|
||||
errStream.Capture();
|
||||
|
||||
_psi.RedirectStandardOutput = true;
|
||||
_psi.RedirectStandardError = true;
|
||||
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = _psi
|
||||
};
|
||||
|
||||
process.EnableRaisingEvents = true;
|
||||
|
||||
process.Start();
|
||||
|
||||
var taskOut = outStream.BeginRead(process.StandardOutput);
|
||||
var taskErr = errStream.BeginRead(process.StandardError);
|
||||
|
||||
process.WaitForExit();
|
||||
|
||||
taskOut.Wait();
|
||||
taskErr.Wait();
|
||||
|
||||
stdOut = outStream.CapturedOutput;
|
||||
stdErr = errStream.CapturedOutput;
|
||||
|
||||
return process.ExitCode;
|
||||
}
|
||||
|
||||
internal ProcessStartInfo _psi;
|
||||
private DotnetCliRunner(string commandName, IEnumerable<string> args)
|
||||
{
|
||||
_psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = commandName,
|
||||
Arguments = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args),
|
||||
UseShellExecute = false,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.General;
|
||||
|
||||
internal static class ProjectModelHelper
|
||||
{
|
||||
internal static Dictionary<string, string> ShortTfmDictionary = new Dictionary<string, string>()
|
||||
{
|
||||
{ ".NETCoreApp,Version=v3.1", "netcoreapp3.1" },
|
||||
{ ".NETCoreApp,Version=v5.0", "net5.0" },
|
||||
{ ".NETCoreApp,Version=v6.0", "net6.0" },
|
||||
{ ".NETCoreApp,Version=v2.1", "netcoreapp2.1" },
|
||||
{ ".NETCoreApp,Version=v7.0", "net7.0" },
|
||||
{ ".NETCoreApp,Version=v8.0", "net8.0" },
|
||||
{ ".NETCoreApp,Version=v9.0", "net9.0" },
|
||||
};
|
||||
|
||||
internal static bool IsTfmPreRelease(string tfm)
|
||||
{
|
||||
return tfm.Equals("net9.0", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal static string GetManifestResource(Assembly assembly, string shortResourceName)
|
||||
{
|
||||
string jsonText = string.Empty;
|
||||
var resourceNames = assembly.GetManifestResourceNames();
|
||||
var resourceName = resourceNames?.FirstOrDefault(x => x.EndsWith(shortResourceName));
|
||||
if (assembly != null && !string.IsNullOrEmpty(resourceName))
|
||||
{
|
||||
Stream? stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
using (StreamReader reader = new StreamReader(stream))
|
||||
{
|
||||
jsonText = reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
return jsonText;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.General;
|
||||
|
||||
public static class StringUtil
|
||||
{
|
||||
//converts Project.Namespace.SubNamespace to Project//Namespace//SubNamespace or Project\\Namespace\\SubNamespace (based on OS)
|
||||
public static string ToPath(string namespaceName, string basePath, string projectRootNamespace)
|
||||
{
|
||||
string path = string.Empty;
|
||||
if (string.IsNullOrEmpty(namespaceName) || string.IsNullOrEmpty(basePath))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
namespaceName = RemovePrefix(namespaceName, basePath, projectRootNamespace);
|
||||
namespaceName = namespaceName.Replace(".", Path.DirectorySeparatorChar.ToString());
|
||||
try
|
||||
{
|
||||
basePath = Path.HasExtension(basePath) ? Path.GetDirectoryName(basePath) ?? basePath : basePath;
|
||||
var combinedPath = Path.Combine(basePath, namespaceName);
|
||||
path = Path.GetFullPath(combinedPath);
|
||||
}
|
||||
//invalid path
|
||||
catch (Exception ex) when (ex is ArgumentException || ex is PathTooLongException || ex is NotSupportedException)
|
||||
{ }
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
//remove prefix from namespace, used to remove the project name from namespace when creating the path
|
||||
public static string RemovePrefix(string projectNamespace, string basePath, string prefix)
|
||||
{
|
||||
string[] namespaceParts = projectNamespace.Split('.');
|
||||
string[] basePathParts = basePath.Split(new char[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (namespaceParts.Length > 0 && namespaceParts[0] == prefix && basePathParts[basePathParts.Length - 1] == prefix)
|
||||
{
|
||||
projectNamespace = string.Join(".", namespaceParts, 1, namespaceParts.Length - 1);
|
||||
}
|
||||
|
||||
return projectNamespace;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// expecting unrooted paths (no drive letter)
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToNamespace(string path)
|
||||
{
|
||||
string ns = path ?? "";
|
||||
if (!string.IsNullOrEmpty(ns))
|
||||
{
|
||||
ns = Path.HasExtension(ns) ? Path.GetDirectoryName(ns) ?? ns : ns;
|
||||
if (ns.Contains(Path.DirectorySeparatorChar))
|
||||
{
|
||||
ns = ns.Replace(Path.DirectorySeparatorChar.ToString(), ".");
|
||||
}
|
||||
|
||||
if (ns.Last() == '.')
|
||||
{
|
||||
ns = ns.Remove(ns.Length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return ns;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// get the full file path without extension, not just the file name
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetFilePathWithoutExtension(string input)
|
||||
{
|
||||
string path = string.Empty;
|
||||
if (!string.IsNullOrEmpty(input))
|
||||
{
|
||||
path = Path.ChangeExtension(input, null);
|
||||
if (path.Contains(Path.DirectorySeparatorChar))
|
||||
{
|
||||
path = path.Replace(Path.DirectorySeparatorChar.ToString(), ".").TrimEnd();
|
||||
}
|
||||
|
||||
if (path.EndsWith("."))
|
||||
{
|
||||
path = path.Remove(path.Length - 1);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
public static string GetTypeNameFromNamespace(string templateName)
|
||||
{
|
||||
string[] parts = templateName.Split('.');
|
||||
return parts[parts.Length - 1];
|
||||
}
|
||||
|
||||
public static string NormalizeLineEndings(string text)
|
||||
{
|
||||
//change all line endings to "\n" and then replace them with the appropriate ending
|
||||
return text.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", System.Environment.NewLine);
|
||||
}
|
||||
|
||||
public static List<string> ToStringList(this List<string?> listWithNullableStrings)
|
||||
{
|
||||
return listWithNullableStrings.ConvertAll(s => s ?? string.Empty).Where(s => s.Equals(string.Empty)).ToList();
|
||||
}
|
||||
|
||||
public static Dictionary<string, string> ParseArguments(List<string> args)
|
||||
{
|
||||
var arguments = new Dictionary<string, string>();
|
||||
if (args != null && args.Count > 0)
|
||||
{
|
||||
string currentKey = string.Empty;
|
||||
foreach (var arg in args)
|
||||
{
|
||||
if (arg.StartsWith("--") || arg.StartsWith("-"))
|
||||
{
|
||||
currentKey = arg;
|
||||
arguments[currentKey] = string.Empty;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(currentKey))
|
||||
{
|
||||
arguments[currentKey] = arg;
|
||||
currentKey = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use enumeration to get file name if they already exist on disk.
|
||||
/// For example, if File.cs exists, try File1.cs and so forth.
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetUniqueFilePath(string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return filePath;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(filePath);
|
||||
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath);
|
||||
var fileExtension = Path.GetExtension(filePath);
|
||||
|
||||
if (string.IsNullOrEmpty(directory) || string.IsNullOrEmpty(fileNameWithoutExtension))
|
||||
{
|
||||
return filePath;
|
||||
}
|
||||
|
||||
int count = 1;
|
||||
string newFilePath;
|
||||
do
|
||||
{
|
||||
newFilePath = Path.Combine(directory, $"{fileNameWithoutExtension}{count}{fileExtension}");
|
||||
count++;
|
||||
} while (File.Exists(newFilePath));
|
||||
|
||||
return newFilePath;
|
||||
}
|
||||
|
||||
public static string EnsureCsExtension(string filePath)
|
||||
{
|
||||
if (!filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
filePath += ".cs";
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)eng\Versions.Scaffold.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Build.Locator" Version="$(MicrosoftBuildLocatorPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Build" Version="$(MicrosoftBuildPackageVersion)" ExcludeAssets="runtime" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="$(CodeAnalysisVersion)" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(CodeAnalysisVersion)" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="$(CodeAnalysisVersion)" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(CodeAnalysisVersion)" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Features" Version="$(CodeAnalysisVersion)" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="$(CodeAnalysisVersion)" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="$(CodeAnalysisVersion)" />
|
||||
<PackageReference Include="Mono.TextTemplating" Version="$(MonoTextTemplatingVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="$(MicrosoftExtensionsDependencyModelPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Setup.Configuration.Interop" PrivateAssets="runtime" Version ="$(MicrosoftVisualStudioSetupConfigurationInteropPackageVersion)">
|
||||
<!-- This package is mostly COM definitions which can be loaded in .NET 6 runtime just fine -->
|
||||
<NoWarn>NU1701</NoWarn>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(RepoRoot)src\Shared\Microsoft.DotNet.Scaffolding.ComponentModel\Microsoft.DotNet.Scaffolding.ComponentModel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.General;
|
||||
|
||||
internal sealed class ProcessOutputStreamReader : IDisposable
|
||||
{
|
||||
private const char FlushBuilderCharacter = '\n';
|
||||
|
||||
private static readonly char[] IgnoreCharacters = [ '\r' ];
|
||||
|
||||
private StringBuilder? _builder;
|
||||
private StringWriter? _capture;
|
||||
|
||||
public string? CapturedOutput => _capture?.GetStringBuilder()?.ToString();
|
||||
|
||||
public ProcessOutputStreamReader Capture()
|
||||
{
|
||||
ThrowIfCaptureSet();
|
||||
|
||||
_capture = new StringWriter();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Task BeginRead(TextReader reader)
|
||||
{
|
||||
return Task.Run(() => Read(reader));
|
||||
}
|
||||
|
||||
public void Read(TextReader reader)
|
||||
{
|
||||
var bufferSize = 1;
|
||||
|
||||
char currentCharacter;
|
||||
|
||||
var buffer = new char[bufferSize];
|
||||
_builder = new StringBuilder();
|
||||
|
||||
if (reader is not null)
|
||||
{
|
||||
// Using Read with buffer size 1 to prevent looping endlessly
|
||||
// like we would when using Read() with no buffer
|
||||
while (reader.Read(buffer, 0, bufferSize) > 0)
|
||||
{
|
||||
currentCharacter = buffer[0];
|
||||
|
||||
if (currentCharacter == FlushBuilderCharacter)
|
||||
{
|
||||
WriteBuilder();
|
||||
}
|
||||
else if (!IgnoreCharacters.Contains(currentCharacter))
|
||||
{
|
||||
_builder.Append(currentCharacter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush anything else when the stream is closed
|
||||
// Which should only happen if someone used console.Write
|
||||
WriteBuilder();
|
||||
|
||||
void WriteBuilder()
|
||||
{
|
||||
if (_builder.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WriteLine(_builder.ToString());
|
||||
_builder.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteLine(string str)
|
||||
{
|
||||
_capture?.WriteLine(str);
|
||||
}
|
||||
|
||||
private void ThrowIfCaptureSet()
|
||||
{
|
||||
if (_capture != null)
|
||||
{
|
||||
throw new InvalidOperationException("Already capturing stream!"); // TODO: Localize this?
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_capture?.Dispose();
|
||||
_capture = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using System.Xml;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Formatting;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn;
|
||||
|
||||
public static class ClassModifier
|
||||
{
|
||||
public static CompilationUnitSyntax AddUsings(CompilationUnitSyntax docRoot, string[] usings)
|
||||
{
|
||||
var usingNodes = CreateUsings(usings);
|
||||
if (usingNodes.Length != 0 && docRoot.Usings.Count == 0)
|
||||
{
|
||||
return docRoot.WithUsings(SyntaxFactory.List(usingNodes));
|
||||
}
|
||||
else
|
||||
{
|
||||
var uniqueUsings = GetUniqueUsings(docRoot.Usings.ToArray(), usingNodes);
|
||||
return uniqueUsings.Any() ? docRoot.WithUsings(docRoot.Usings.AddRange(uniqueUsings)) : docRoot;
|
||||
}
|
||||
}
|
||||
|
||||
public static UsingDirectiveSyntax[] CreateUsings(string[] usings)
|
||||
{
|
||||
var usingDirectiveList = new List<UsingDirectiveSyntax>();
|
||||
if (usings is null)
|
||||
{
|
||||
return usingDirectiveList.ToArray();
|
||||
}
|
||||
|
||||
var nameLeadingTrivia = SyntaxFactory.TriviaList(SyntaxFactory.Space);
|
||||
var usingTrailingTrivia = SyntaxFactory.TriviaList(SyntaxFactory.CarriageReturnLineFeed);
|
||||
foreach (var usingDirectiveString in usings)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(usingDirectiveString) && !usingDirectiveString.Equals("<global namespace>", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
//leading space on the value of the using eg. (using' 'Microsoft.Yadada)
|
||||
var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName(usingDirectiveString)
|
||||
.WithLeadingTrivia(nameLeadingTrivia))
|
||||
.WithAdditionalAnnotations(Formatter.Annotation)
|
||||
.WithTrailingTrivia(usingTrailingTrivia);
|
||||
|
||||
usingDirectiveList.Add(usingDirective);
|
||||
}
|
||||
}
|
||||
|
||||
return usingDirectiveList.ToArray();
|
||||
}
|
||||
|
||||
public static SyntaxList<UsingDirectiveSyntax> GetUniqueUsings(UsingDirectiveSyntax[] existingUsings, UsingDirectiveSyntax[] newUsings)
|
||||
{
|
||||
return SyntaxFactory.List(
|
||||
newUsings.Where(u => u.Name != null && !existingUsings.Any(oldUsing => oldUsing.Name != null && oldUsing.Name.ToString().Equals(u.Name.ToString())))
|
||||
.OrderBy(us => us.Name?.ToString()));
|
||||
}
|
||||
|
||||
public static ClassDeclarationSyntax GetNewStaticClassDeclaration(string className)
|
||||
{
|
||||
return SyntaxFactory.ClassDeclaration(className)
|
||||
.WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword)))
|
||||
.NormalizeWhitespace()
|
||||
.WithLeadingTrivia(SyntaxFactory.CarriageReturnLineFeed, SyntaxFactory.CarriageReturnLineFeed);
|
||||
}
|
||||
|
||||
public static CompilationUnitSyntax GetNewStaticCompilationUnit(string className)
|
||||
{
|
||||
var classDeclaration = GetNewStaticClassDeclaration(className);
|
||||
return SyntaxFactory.CompilationUnit()
|
||||
.AddMembers(classDeclaration);
|
||||
}
|
||||
|
||||
public static void RemoveCompileIncludes(string? projectCsprojPath, List<string> excludeList)
|
||||
{
|
||||
if (string.IsNullOrEmpty(projectCsprojPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the project file
|
||||
var doc = new XmlDocument();
|
||||
doc.Load(projectCsprojPath);
|
||||
|
||||
if (doc.DocumentElement is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the default namespace for the project file
|
||||
var nsMgr = new XmlNamespaceManager(doc.NameTable);
|
||||
nsMgr.AddNamespace("ns", doc.DocumentElement.NamespaceURI);
|
||||
|
||||
// Find all <Compile> elements to remove
|
||||
foreach (var include in excludeList)
|
||||
{
|
||||
var compileNode = doc.SelectSingleNode(
|
||||
$"/ns:Project/ns:ItemGroup/ns:Compile[@Include='{include}']",
|
||||
nsMgr);
|
||||
|
||||
// If the <Compile> element exists, remove it
|
||||
if (compileNode != null && compileNode.ParentNode != null)
|
||||
{
|
||||
compileNode.ParentNode.RemoveChild(compileNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty <ItemGroup> elements
|
||||
var itemGroups = doc.SelectNodes("/ns:Project/ns:ItemGroup", nsMgr);
|
||||
if (itemGroups != null && itemGroups.Count != 0)
|
||||
{
|
||||
foreach (XmlNode itemGroup in itemGroups)
|
||||
{
|
||||
if (itemGroup.ChildNodes.Count == 0 && itemGroup.ParentNode != null)
|
||||
{
|
||||
itemGroup.ParentNode.RemoveChild(itemGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the modified project file
|
||||
doc.Save(projectCsprojPath);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn
|
||||
{
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum CodeChangeType
|
||||
{
|
||||
Default,
|
||||
MemberAccess,
|
||||
Lambda
|
||||
}
|
||||
|
||||
public class CodeChangeOptionStrings
|
||||
{
|
||||
public const string IfStatement = nameof(IfStatement);
|
||||
public const string ElseStatement = nameof(ElseStatement);
|
||||
public const string MicrosoftGraph = nameof(MicrosoftGraph);
|
||||
public const string DownstreamApi = nameof(DownstreamApi);
|
||||
public const string Skip = nameof(Skip);
|
||||
public const string NonMinimalApp = nameof(NonMinimalApp);
|
||||
public const string MinimalApp = nameof(MinimalApp);
|
||||
public const string OpenApi = nameof(OpenApi);
|
||||
}
|
||||
|
||||
public class CodeChangeOptions
|
||||
{
|
||||
public bool MicrosoftGraph { get; set; } = false;
|
||||
public bool DownstreamApi { get; set; } = false;
|
||||
public bool IsMinimalApp { get; set; } = false;
|
||||
public bool UsingTopLevelsStatements { get; set; } = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn;
|
||||
|
||||
public class CodeFile
|
||||
{
|
||||
public Dictionary<string, Method>? Methods { get; set; }
|
||||
public CodeSnippet[]? Replacements { get; set; }
|
||||
public string? AddFilePath { get; set; }
|
||||
public string[]? Usings { get; set; }
|
||||
public CodeBlock[]? UsingsWithOptions { get; set; }
|
||||
public string? FileName { get; set; }
|
||||
public string? Extension => FileName?.Substring(FileName.LastIndexOf('.') + 1);
|
||||
public CodeBlock[]? ClassProperties { get; set; }
|
||||
public CodeBlock[]? ClassAttributes { get; set; }
|
||||
public string[]? GlobalVariables { get; set; }
|
||||
public string[]? Options { get; set; }
|
||||
}
|
||||
|
||||
public class CodeBlock
|
||||
{
|
||||
public string? Block { get; set; }
|
||||
public string[]? Options { get; set; }
|
||||
}
|
||||
|
||||
public class Formatting
|
||||
{
|
||||
public bool Newline { get; set; }
|
||||
public int NumberOfSpaces { get; set; }
|
||||
public bool Semicolon { get; set; }
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn
|
||||
{
|
||||
public class CodeSnippet
|
||||
{
|
||||
public string? InsertAfter { get; set; }
|
||||
private string _block = default!;
|
||||
public string Block
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(_block) && MultiLineBlock is not null)
|
||||
{
|
||||
_block = string.Join(System.Environment.NewLine, MultiLineBlock);
|
||||
}
|
||||
|
||||
return _block;
|
||||
}
|
||||
set { _block = value; }
|
||||
}
|
||||
|
||||
public string? CheckBlock { get; set; }
|
||||
public string? Parent { get; set; }
|
||||
public bool Prepend { get; set; } = false;
|
||||
public bool Replace { get; set; } = false;
|
||||
public string[]? InsertBefore { get; set; }
|
||||
public string[]? Options { get; set; }
|
||||
public Formatting LeadingTrivia { get; set; } = new Formatting();
|
||||
public Formatting TrailingTrivia { get; set; } = new Formatting { Semicolon = true, Newline = true };
|
||||
public string? Parameter { get; set; }
|
||||
public CodeChangeType? CodeChangeType { get; set; } = Roslyn.CodeChangeType.Default;
|
||||
public string[]? MultiLineBlock { get; set; }
|
||||
public string[]? ReplaceSnippet { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn
|
||||
{
|
||||
public class Method
|
||||
{
|
||||
public string[]? Parameters { get; set; }
|
||||
public CodeBlock[]? AddParameters { get; set; }
|
||||
public CodeBlock? EditType { get; set; }
|
||||
public CodeSnippet[]? CodeChanges { get; set; }
|
||||
public CodeBlock[]? Attributes { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn;
|
||||
|
||||
public class CodeModifierConfig
|
||||
{
|
||||
public string? Identifier { get; set; }
|
||||
public CodeFile[]? Files { get; set; }
|
||||
}
|
|
@ -0,0 +1,995 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Formatting;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.General;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn;
|
||||
|
||||
public class DocumentBuilder
|
||||
{
|
||||
private readonly CodeFile _codeFile;
|
||||
private readonly ILogger _consoleLogger;
|
||||
private readonly Document _document;
|
||||
|
||||
public DocumentBuilder(
|
||||
Document document,
|
||||
CodeFile codeFile,
|
||||
ILogger consoleLogger)
|
||||
{
|
||||
_document = document ?? throw new ArgumentNullException(nameof(document));
|
||||
_codeFile = codeFile ?? throw new ArgumentNullException(nameof(codeFile));
|
||||
_consoleLogger = consoleLogger ?? throw new ArgumentNullException(nameof(consoleLogger));
|
||||
}
|
||||
|
||||
public async Task<Document> RunAsync()
|
||||
{
|
||||
var document = _document;
|
||||
var syntaxRoot = await document.GetSyntaxRootAsync() as CompilationUnitSyntax;
|
||||
var modifiedRoot = ModifyRoot(syntaxRoot, new CodeChangeOptions());
|
||||
if (modifiedRoot != null)
|
||||
{
|
||||
return document.WithSyntaxRoot(modifiedRoot);
|
||||
}
|
||||
|
||||
//return the same document if syntax root came back null.
|
||||
return document;
|
||||
}
|
||||
|
||||
internal static BaseMethodDeclarationSyntax GetModifiedMethod(string fileName, BaseMethodDeclarationSyntax method, Method methodChanges, CodeChangeOptions options, StringBuilder? output)
|
||||
{
|
||||
method = ModifyMethodAttributes(method, methodChanges, options);
|
||||
method = AddCodeSnippetsToMethod(fileName, method, methodChanges, options, output);
|
||||
method = EditMethodReturnType(method, methodChanges, options);
|
||||
method = AddMethodParameters(method, methodChanges, options);
|
||||
return method;
|
||||
}
|
||||
|
||||
private static BaseMethodDeclarationSyntax ModifyMethodAttributes(BaseMethodDeclarationSyntax method, Method methodChanges, CodeChangeOptions options)
|
||||
{
|
||||
if (methodChanges.Attributes != null && methodChanges.Attributes.Any())
|
||||
{
|
||||
var attributes = ProjectModifierHelper.FilterCodeBlocks(methodChanges.Attributes, options);
|
||||
var methodAttributes = CreateAttributeList(attributes, method.AttributeLists, method.GetLeadingTrivia());
|
||||
|
||||
method = method.WithAttributeLists(methodAttributes);
|
||||
}
|
||||
|
||||
return method;
|
||||
}
|
||||
|
||||
public CompilationUnitSyntax? ModifyRoot(CompilationUnitSyntax? root, CodeChangeOptions options)
|
||||
{
|
||||
if (root is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
root = AddUsings(root, options);
|
||||
if (!string.IsNullOrEmpty(_codeFile.FileName) && _codeFile.FileName.Equals("Program.cs") &&
|
||||
_codeFile.Methods != null && _codeFile.Methods.TryGetValue("Global", out var globalChanges))
|
||||
{
|
||||
var filteredChanges = ProjectModifierHelper.FilterCodeSnippets(globalChanges.CodeChanges, options);
|
||||
var updatedIdentifer = ProjectModifierHelper.GetBuilderVariableIdentifierTransformation(root.Members);
|
||||
if (updatedIdentifer.HasValue)
|
||||
{
|
||||
(string oldValue, string newValue) = updatedIdentifer.Value;
|
||||
filteredChanges = ProjectModifierHelper.UpdateVariables(filteredChanges, oldValue, newValue);
|
||||
}
|
||||
|
||||
if (!options.UsingTopLevelsStatements)
|
||||
{
|
||||
var mainMethod = root?.DescendantNodes().OfType<MethodDeclarationSyntax>()
|
||||
.FirstOrDefault(n => ProjectModifierHelper.Main.Equals(n.Identifier.ToString(), StringComparison.OrdinalIgnoreCase));
|
||||
if (mainMethod != null
|
||||
&& ApplyChangesToMethod(mainMethod.Body, filteredChanges, _codeFile.FileName) is BlockSyntax updatedBody)
|
||||
{
|
||||
var updatedMethod = mainMethod.WithBody(updatedBody);
|
||||
return root?.ReplaceNode(mainMethod, updatedMethod);
|
||||
}
|
||||
}
|
||||
else if (root.Members.Any(node => node.IsKind(SyntaxKind.GlobalStatement)))
|
||||
{
|
||||
return ApplyChangesToMethod(root, filteredChanges, _codeFile.FileName) as CompilationUnitSyntax;
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(_codeFile.FileName))
|
||||
{
|
||||
var namespaceNode = root?.Members.OfType<BaseNamespaceDeclarationSyntax>()?.FirstOrDefault();
|
||||
string className = ProjectModifierHelper.GetClassName(_codeFile.FileName);
|
||||
// get classNode. All class changes are done on the ClassDeclarationSyntax and then that node is replaced using documentEditor.
|
||||
var classDeclarationSyntax = namespaceNode?.
|
||||
DescendantNodes().
|
||||
OfType<ClassDeclarationSyntax>().
|
||||
FirstOrDefault(node =>
|
||||
node.Identifier.ValueText.Contains(className));
|
||||
if (classDeclarationSyntax != null)
|
||||
{
|
||||
var modifiedClassDeclarationSyntax = classDeclarationSyntax;
|
||||
//add class properties
|
||||
modifiedClassDeclarationSyntax = AddProperties(modifiedClassDeclarationSyntax, options);
|
||||
//add class attributes
|
||||
modifiedClassDeclarationSyntax = AddClassAttributes(modifiedClassDeclarationSyntax, options);
|
||||
//add code snippets/changes.
|
||||
if (_codeFile.Methods != null)
|
||||
{
|
||||
modifiedClassDeclarationSyntax = ModifyMethods(_codeFile.FileName, modifiedClassDeclarationSyntax, _codeFile.Methods, options);
|
||||
}
|
||||
|
||||
if (root is SyntaxNode syntaxRoot)
|
||||
{
|
||||
root = root.ReplaceNode(classDeclarationSyntax, modifiedClassDeclarationSyntax);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
public CompilationUnitSyntax AddUsings(CompilationUnitSyntax docRoot, CodeChangeOptions options)
|
||||
{
|
||||
// adding usings
|
||||
if (_codeFile.UsingsWithOptions != null && _codeFile.UsingsWithOptions.Any())
|
||||
{
|
||||
var usingsWithOptions = FilterUsingsWithOptions(_codeFile, options);
|
||||
_codeFile.Usings = _codeFile.Usings?.Concat(usingsWithOptions).ToArray() ?? usingsWithOptions.ToArray();
|
||||
}
|
||||
|
||||
var usingNodes = CreateUsings(_codeFile.Usings);
|
||||
if (usingNodes.Any() && docRoot.Usings.Count == 0)
|
||||
{
|
||||
return docRoot.WithUsings(SyntaxFactory.List(usingNodes));
|
||||
}
|
||||
else
|
||||
{
|
||||
var uniqueUsings = GetUniqueUsings(docRoot.Usings.ToArray(), usingNodes);
|
||||
return uniqueUsings.Any() ? docRoot.WithUsings(docRoot.Usings.AddRange(uniqueUsings)) : docRoot;
|
||||
}
|
||||
}
|
||||
|
||||
internal static SyntaxList<UsingDirectiveSyntax> GetUniqueUsings(UsingDirectiveSyntax[] existingUsings, UsingDirectiveSyntax[] newUsings)
|
||||
{
|
||||
return SyntaxFactory.List(
|
||||
newUsings.Where(u => !existingUsings.Any(oldUsing => oldUsing.Name != null && oldUsing.Name.ToString().Equals(u.Name?.ToString())))
|
||||
.OrderBy(us => us.Name?.ToString()));
|
||||
}
|
||||
|
||||
internal static IList<string> FilterUsingsWithOptions(CodeFile codeFile, CodeChangeOptions options)
|
||||
{
|
||||
List<string> usingsWithOptions = [];
|
||||
if (codeFile != null)
|
||||
{
|
||||
var filteredCodeBlocks = codeFile.UsingsWithOptions?.Where(us => ProjectModifierHelper.FilterOptions(us.Options, options)).ToList();
|
||||
if (filteredCodeBlocks != null && filteredCodeBlocks.Count != 0)
|
||||
{
|
||||
usingsWithOptions = filteredCodeBlocks.Select(cb => cb.Block).ToList().ToStringList();
|
||||
}
|
||||
}
|
||||
|
||||
return usingsWithOptions;
|
||||
}
|
||||
|
||||
//Add class members to the top of the class.
|
||||
public ClassDeclarationSyntax AddProperties(ClassDeclarationSyntax classDeclarationSyntax, CodeChangeOptions options)
|
||||
{
|
||||
var modifiedClassDeclarationSyntax = classDeclarationSyntax;
|
||||
if (_codeFile.ClassProperties != null && _codeFile.ClassProperties.Any() && classDeclarationSyntax != null)
|
||||
{
|
||||
_codeFile.ClassProperties = ProjectModifierHelper.FilterCodeBlocks(_codeFile.ClassProperties, options);
|
||||
//get a sample member for leading trivia. Trailing trivia will still be semi colon and new line.
|
||||
var sampleMember = modifiedClassDeclarationSyntax.Members.FirstOrDefault();
|
||||
var memberLeadingTrivia = sampleMember?.GetLeadingTrivia() ?? SyntaxFactory.TriviaList(SyntaxFactory.Whitespace(" "));
|
||||
var memberTrailingTrivia = SyntaxFactory.TriviaList(SemiColonTrivia, SyntaxFactory.CarriageReturnLineFeed);
|
||||
|
||||
//create MemberDeclarationSyntax[] with all the Property strings.
|
||||
var classProperties = CreateClassProperties(modifiedClassDeclarationSyntax.Members, memberLeadingTrivia, memberTrailingTrivia);
|
||||
|
||||
if (classProperties.Length > 0)
|
||||
{
|
||||
//add to the top of the class.
|
||||
var members = modifiedClassDeclarationSyntax.Members;
|
||||
foreach (var property in classProperties)
|
||||
{
|
||||
members = members.Insert(0, property);
|
||||
}
|
||||
|
||||
modifiedClassDeclarationSyntax = modifiedClassDeclarationSyntax
|
||||
.WithMembers(members);
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedClassDeclarationSyntax;
|
||||
}
|
||||
|
||||
//Add class attributes '[Attribute]' to a class.
|
||||
public ClassDeclarationSyntax AddClassAttributes(ClassDeclarationSyntax classDeclarationSyntax, CodeChangeOptions options)
|
||||
{
|
||||
var modifiedClassDeclarationSyntax = classDeclarationSyntax;
|
||||
if (_codeFile.ClassAttributes != null && _codeFile.ClassAttributes.Any() && classDeclarationSyntax != null)
|
||||
{
|
||||
_codeFile.ClassAttributes = ProjectModifierHelper.FilterCodeBlocks(_codeFile.ClassAttributes, options);
|
||||
var classAttributes = CreateAttributeList(_codeFile.ClassAttributes, modifiedClassDeclarationSyntax.AttributeLists, classDeclarationSyntax.GetLeadingTrivia());
|
||||
modifiedClassDeclarationSyntax = modifiedClassDeclarationSyntax.WithAttributeLists(classAttributes);
|
||||
}
|
||||
return modifiedClassDeclarationSyntax;
|
||||
}
|
||||
|
||||
internal static ClassDeclarationSyntax ModifyMethods(string fileName, ClassDeclarationSyntax classNode, Dictionary<string, Method> methods, CodeChangeOptions options, StringBuilder? output = null)
|
||||
{
|
||||
foreach ((string methodName, Method methodChanges) in methods)
|
||||
{
|
||||
if (methodChanges is null || methodChanges.Parameters is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var methodNode = ProjectModifierHelper.GetOriginalMethod(classNode, methodName, methodChanges);
|
||||
if (methodNode is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var parameters = ProjectModifierHelper.VerifyParameters(methodChanges.Parameters, methodNode.ParameterList.Parameters.ToList());
|
||||
foreach ((string oldValue, string newValue) in parameters)
|
||||
{
|
||||
methodChanges.CodeChanges = ProjectModifierHelper.UpdateVariables(methodChanges.CodeChanges, oldValue, newValue);
|
||||
}
|
||||
|
||||
var updatedMethodNode = GetModifiedMethod(fileName, methodNode, methodChanges, options, output);
|
||||
if (updatedMethodNode != null)
|
||||
{
|
||||
classNode = classNode.ReplaceNode(methodNode, updatedMethodNode);
|
||||
}
|
||||
}
|
||||
|
||||
return classNode;
|
||||
}
|
||||
|
||||
internal static BaseMethodDeclarationSyntax AddMethodParameters(BaseMethodDeclarationSyntax originalMethod, Method methodChanges, CodeChangeOptions options)
|
||||
{
|
||||
if (methodChanges is null || methodChanges.AddParameters is null || !methodChanges.AddParameters.Any())
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
// Filter for CodeChangeOptions
|
||||
methodChanges.AddParameters = ProjectModifierHelper.FilterCodeBlocks(methodChanges.AddParameters, options);
|
||||
return AddParameters(originalMethod, methodChanges.AddParameters, options);
|
||||
}
|
||||
|
||||
// Add all the different code snippet.
|
||||
internal static BaseMethodDeclarationSyntax AddCodeSnippetsToMethod(string fileName, BaseMethodDeclarationSyntax originalMethod, Method methodChanges, CodeChangeOptions options, StringBuilder? output)
|
||||
{
|
||||
if (methodChanges.CodeChanges == null || !methodChanges.CodeChanges.Any())
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
var filteredChanges = ProjectModifierHelper.FilterCodeSnippets(methodChanges.CodeChanges, options);
|
||||
|
||||
if (filteredChanges is null || !filteredChanges.Any())
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
var blockSyntax = originalMethod.Body;
|
||||
var modifiedMethod = ApplyChangesToMethod(blockSyntax, filteredChanges, fileName, output);
|
||||
|
||||
if (blockSyntax != null && modifiedMethod != null)
|
||||
{
|
||||
return originalMethod.ReplaceNode(blockSyntax, modifiedMethod);
|
||||
}
|
||||
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
internal static SyntaxNode? ApplyChangesToMethod(SyntaxNode? root, CodeSnippet[]? filteredChanges, string? fileName = null, StringBuilder? output = null)
|
||||
{
|
||||
if (root is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
bool changesMade = false;
|
||||
if (filteredChanges is null)
|
||||
{
|
||||
return root;
|
||||
}
|
||||
|
||||
foreach (var change in filteredChanges)
|
||||
{
|
||||
var update = ModifyMethod(root, change, output);
|
||||
if (update != null)
|
||||
{
|
||||
changesMade = true;
|
||||
root = root.ReplaceNode(root, update);
|
||||
}
|
||||
}
|
||||
|
||||
if (!changesMade)
|
||||
{
|
||||
output?.AppendLine(value: $"No modifications made for file: {fileName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
output?.AppendLine($"Modified {fileName}");
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
internal static MethodDeclarationSyntax? GetMethodFromSyntaxRoot(CompilationUnitSyntax root, string methodIdentifier)
|
||||
{
|
||||
BaseNamespaceDeclarationSyntax? namespaceNode = root.Members.OfType<NamespaceDeclarationSyntax>()?.FirstOrDefault();
|
||||
if (namespaceNode == null)
|
||||
{
|
||||
namespaceNode = root.Members.OfType<FileScopedNamespaceDeclarationSyntax>()?.FirstOrDefault();
|
||||
}
|
||||
|
||||
var classNode = namespaceNode?.Members.OfType<ClassDeclarationSyntax>()?.FirstOrDefault() ??
|
||||
root?.Members.OfType<ClassDeclarationSyntax>()?.FirstOrDefault();
|
||||
if (classNode?.ChildNodes().FirstOrDefault(
|
||||
n => n is MethodDeclarationSyntax syntax &&
|
||||
syntax.Identifier.ToString().Equals(methodIdentifier, StringComparison.OrdinalIgnoreCase)) is MethodDeclarationSyntax method)
|
||||
{
|
||||
return method;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static CodeSnippet AddLeadingTriviaSpaces(CodeSnippet snippet, int spaces)
|
||||
{
|
||||
snippet.LeadingTrivia = snippet.LeadingTrivia ?? new Formatting();
|
||||
snippet.LeadingTrivia.NumberOfSpaces += spaces;
|
||||
return snippet;
|
||||
}
|
||||
|
||||
internal static CodeSnippet[] AddLeadingTriviaSpaces(CodeSnippet[] snippets, int spaces)
|
||||
{
|
||||
for (int i = 0; i < snippets.Length; i++)
|
||||
{
|
||||
var snippet = snippets[i];
|
||||
snippet = AddLeadingTriviaSpaces(snippet, spaces);
|
||||
snippets[i] = snippet;
|
||||
}
|
||||
|
||||
return snippets;
|
||||
}
|
||||
|
||||
internal static SyntaxNode ModifyMethod(SyntaxNode originalMethod, CodeSnippet codeChange, StringBuilder? output = null)
|
||||
{
|
||||
SyntaxNode? modifiedMethod = null;
|
||||
try
|
||||
{
|
||||
switch (codeChange.CodeChangeType)
|
||||
{
|
||||
case CodeChangeType.Lambda:
|
||||
{
|
||||
modifiedMethod = AddOrUpdateLambda(originalMethod, codeChange);
|
||||
break;
|
||||
}
|
||||
case CodeChangeType.MemberAccess:
|
||||
{
|
||||
modifiedMethod = AddExpressionToParent(originalMethod, codeChange);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
modifiedMethod = UpdateMethod(originalMethod, codeChange);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
//output?.Append(value: $"Error modifying method {originalMethod}\nCodeChange:{codeChange.ToJson()}");
|
||||
}
|
||||
|
||||
return modifiedMethod != null ? originalMethod.ReplaceNode(originalMethod, modifiedMethod) : originalMethod;
|
||||
}
|
||||
|
||||
internal static SyntaxNode UpdateMethod(SyntaxNode originalMethod, CodeSnippet codeChange)
|
||||
{
|
||||
var children = GetDescendantNodes(originalMethod);
|
||||
if (children is null)
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
//check for CodeChange.Block and CodeChange.CheckBlock for block's are easy to check.
|
||||
if (ProjectModifierHelper.StatementExists(children, codeChange.Block) || (!string.IsNullOrEmpty(codeChange.CheckBlock) && ProjectModifierHelper.StatementExists(children, codeChange.CheckBlock)))
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
SyntaxNode updatedMethod;
|
||||
if (codeChange.InsertBefore?.Any() is true)
|
||||
{
|
||||
updatedMethod = InsertBefore(codeChange, children, originalMethod);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(codeChange.InsertAfter))
|
||||
{
|
||||
updatedMethod = InsertAfter(codeChange, children, originalMethod);
|
||||
}
|
||||
else
|
||||
{
|
||||
updatedMethod = GetBlockStatement(originalMethod, codeChange);
|
||||
}
|
||||
|
||||
return updatedMethod ?? originalMethod;
|
||||
}
|
||||
|
||||
private static SyntaxNode InsertBefore(CodeSnippet codeChange, IEnumerable<SyntaxNode> children, SyntaxNode originalMethod)
|
||||
{
|
||||
var followingNode = GetFollowingNode(codeChange.InsertBefore, children);
|
||||
if (followingNode is null)
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
var leadingWhitespaceTrivia = followingNode.GetLeadingTrivia().LastOrDefault();
|
||||
if (leadingWhitespaceTrivia.IsKind(SyntaxKind.WhitespaceTrivia))
|
||||
{
|
||||
codeChange.LeadingTrivia.NumberOfSpaces += leadingWhitespaceTrivia.Span.Length;
|
||||
}
|
||||
|
||||
var newNodes = GetNodeInsertionList(codeChange, originalMethod.Kind());
|
||||
if (newNodes is null)
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
return originalMethod.InsertNodesBefore(followingNode, newNodes);
|
||||
}
|
||||
|
||||
|
||||
private static SyntaxNode InsertAfter(CodeSnippet codeChange, IEnumerable<SyntaxNode> children, SyntaxNode originalMethod)
|
||||
{
|
||||
var precedingNode = GetSpecifiedNode(codeChange.InsertAfter, children);
|
||||
if (precedingNode is null)
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
var leadingWhitespaceTrivia = precedingNode.GetLeadingTrivia().LastOrDefault();
|
||||
if (leadingWhitespaceTrivia.IsKind(SyntaxKind.WhitespaceTrivia))
|
||||
{
|
||||
codeChange.LeadingTrivia.NumberOfSpaces += leadingWhitespaceTrivia.Span.Length;
|
||||
}
|
||||
|
||||
var newNodes = GetNodeInsertionList(codeChange, originalMethod.Kind());
|
||||
if (newNodes is null)
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
return originalMethod.InsertNodesAfter(precedingNode, newNodes);
|
||||
}
|
||||
|
||||
private static SyntaxNode? GetFollowingNode(string[]? insertBefore, IEnumerable<SyntaxNode> descendantNodes)
|
||||
{
|
||||
if (insertBefore != null)
|
||||
{
|
||||
foreach (var specifier in insertBefore)
|
||||
{
|
||||
if (GetSpecifiedNode(specifier, descendantNodes) is SyntaxNode insertBeforeNode)
|
||||
{
|
||||
return insertBeforeNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a List of SyntaxNodes with the code change added, useful as an input for methods such as SyntaxNode.InsertNodesBefore that require a list
|
||||
/// Additionally, with this method we can use the same code for StatementSyntax and GlobalStatementSyntax nodes
|
||||
/// </summary>
|
||||
/// <param name="codeChange"></param>
|
||||
/// <param name="syntaxKind"></param>
|
||||
/// <returns></returns>
|
||||
private static List<SyntaxNode>? GetNodeInsertionList(CodeSnippet codeChange, SyntaxKind syntaxKind)
|
||||
{
|
||||
var statement = GetStatementWithTrivia(codeChange);
|
||||
if (statement is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return syntaxKind == SyntaxKind.CompilationUnit
|
||||
? new List<SyntaxNode> { SyntaxFactory.GlobalStatement(statement) }
|
||||
: new List<SyntaxNode> { statement };
|
||||
}
|
||||
|
||||
private static SyntaxNode GetBlockStatement(SyntaxNode node, CodeSnippet codeChange)
|
||||
{
|
||||
var syntaxKind = node.Kind();
|
||||
var statement = GetStatementWithTrivia(codeChange);
|
||||
if (syntaxKind == SyntaxKind.Block && node is BlockSyntax block)
|
||||
{
|
||||
block = codeChange.Prepend
|
||||
? block.WithStatements(block.Statements.Insert(0, statement))
|
||||
: block.AddStatements(statement);
|
||||
|
||||
return block;
|
||||
}
|
||||
if (syntaxKind == SyntaxKind.CompilationUnit && node is CompilationUnitSyntax compilationUnit)
|
||||
{
|
||||
var globalStatement = SyntaxFactory.GlobalStatement(statement);
|
||||
return codeChange.Prepend
|
||||
? compilationUnit.WithMembers(compilationUnit.Members.Insert(0, globalStatement))
|
||||
: compilationUnit.AddMembers(globalStatement);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private static StatementSyntax GetStatementWithTrivia(CodeSnippet change)
|
||||
{
|
||||
var leadingTrivia = GetLeadingTrivia(change.LeadingTrivia);
|
||||
var trailingTrivia = GetTrailingTrivia(change.TrailingTrivia);
|
||||
|
||||
return SyntaxFactory.ParseStatement(change.Block)
|
||||
.WithAdditionalAnnotations(Formatter.Annotation)
|
||||
.WithLeadingTrivia(leadingTrivia)
|
||||
.WithTrailingTrivia(trailingTrivia);
|
||||
}
|
||||
|
||||
private static SyntaxTriviaList GetLeadingTrivia(Formatting codeFormatting)
|
||||
{
|
||||
var statementLeadingTrivia = SyntaxFactory.TriviaList();
|
||||
if (codeFormatting != null)
|
||||
{
|
||||
if (codeFormatting.Newline)
|
||||
{
|
||||
statementLeadingTrivia = statementLeadingTrivia.Add(SyntaxFactory.ElasticCarriageReturnLineFeed);
|
||||
}
|
||||
if (codeFormatting.NumberOfSpaces > 0)
|
||||
{
|
||||
statementLeadingTrivia = statementLeadingTrivia.Add(SyntaxFactory.Whitespace(new string(' ', codeFormatting.NumberOfSpaces)));
|
||||
}
|
||||
}
|
||||
|
||||
return statementLeadingTrivia;
|
||||
}
|
||||
|
||||
private static SyntaxTriviaList GetTrailingTrivia(Formatting codeFormatting)
|
||||
{
|
||||
var statementLeadingTrivia = SyntaxFactory.TriviaList();
|
||||
if (codeFormatting != null)
|
||||
{
|
||||
if (codeFormatting.Semicolon)
|
||||
{
|
||||
statementLeadingTrivia = statementLeadingTrivia.Add(SemiColonTrivia);
|
||||
}
|
||||
if (codeFormatting.Newline)
|
||||
{
|
||||
statementLeadingTrivia = statementLeadingTrivia.Add(SyntaxFactory.ElasticCarriageReturnLineFeed);
|
||||
}
|
||||
}
|
||||
|
||||
return statementLeadingTrivia;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches through list of nodes and returns the first node that contains the specifierStatement
|
||||
/// Note: can be null
|
||||
/// </summary>
|
||||
/// <param name="specifierStatement"></param>
|
||||
/// <param name="descendantNodes"></param>
|
||||
/// <returns></returns>
|
||||
internal static SyntaxNode? GetSpecifiedNode(string? specifierStatement, IEnumerable<SyntaxNode> descendantNodes)
|
||||
{
|
||||
if (string.IsNullOrEmpty(specifierStatement))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO descendantNodes could be null
|
||||
var specifiedDescendant =
|
||||
descendantNodes?.FirstOrDefault(d => d != null && d.ToString().Contains(specifierStatement)) ??
|
||||
descendantNodes?.FirstOrDefault(d => d != null && d.ToString().Contains(ProjectModifierHelper.TrimStatement(specifierStatement)));
|
||||
|
||||
return specifiedDescendant;
|
||||
}
|
||||
|
||||
internal static BaseMethodDeclarationSyntax EditMethodReturnType(BaseMethodDeclarationSyntax originalMethod, Method methodChanges, CodeChangeOptions options)
|
||||
{
|
||||
if (methodChanges is null || methodChanges.EditType is null || !(originalMethod is MethodDeclarationSyntax modifiedMethod))
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
methodChanges.EditType = ProjectModifierHelper.FilterCodeBlocks(new CodeBlock[] { methodChanges.EditType }, options).FirstOrDefault();
|
||||
|
||||
// After filtering, the method type might not need editing
|
||||
if (methodChanges.EditType is null || string.IsNullOrEmpty(methodChanges.EditType.Block))
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
var returnTypeString = modifiedMethod.ReturnType.ToFullString();
|
||||
if (modifiedMethod.Modifiers.Any(m => m.ToFullString().Contains("async")))
|
||||
{
|
||||
returnTypeString = $"async {returnTypeString}";
|
||||
}
|
||||
|
||||
if (!ProjectModifierHelper.TrimStatement(returnTypeString).Equals(ProjectModifierHelper.TrimStatement(methodChanges.EditType.Block)))
|
||||
{
|
||||
var typeIdentifier = SyntaxFactory.IdentifierName(SyntaxFactory.Identifier(methodChanges.EditType.Block));
|
||||
modifiedMethod = modifiedMethod.WithReturnType(typeIdentifier.WithTrailingTrivia(SyntaxFactory.Whitespace(" ")));
|
||||
|
||||
return modifiedMethod;
|
||||
}
|
||||
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
internal static BaseMethodDeclarationSyntax AddParameters(BaseMethodDeclarationSyntax methodNode, CodeBlock[] addParameters, CodeChangeOptions toolOptions)
|
||||
{
|
||||
var newMethod = methodNode;
|
||||
List<ParameterSyntax> newParameters = new List<ParameterSyntax>();
|
||||
foreach (var parameter in addParameters)
|
||||
{
|
||||
if (string.IsNullOrEmpty(parameter.Block))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var identifier = SyntaxFactory.Identifier(parameter.Block).WithLeadingTrivia(SyntaxFactory.Whitespace(" "));
|
||||
var parameterSyntax = SyntaxFactory.Parameter(identifier);
|
||||
if (!newMethod.ParameterList.Parameters.Any(p => p.ToFullString().Equals(parameter.Block)))
|
||||
{
|
||||
newParameters.Add(parameterSyntax);
|
||||
}
|
||||
}
|
||||
|
||||
if (newParameters.Any())
|
||||
{
|
||||
newMethod = newMethod.AddParameterListParameters(newParameters.ToArray());
|
||||
}
|
||||
|
||||
return newMethod;
|
||||
}
|
||||
|
||||
private static SyntaxNode AddOrUpdateLambda(SyntaxNode originalMethod, CodeSnippet change)
|
||||
{
|
||||
var descendants = GetDescendantNodes(originalMethod);
|
||||
if (descendants is null)
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
var parent = GetSpecifiedNode(change.Parent, descendants);
|
||||
if (parent is null)
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
var children = GetDescendantNodes(parent);
|
||||
if (children is null)
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
var updatedParent = parent;
|
||||
// Check for existing lambda
|
||||
if (children.FirstOrDefault(
|
||||
d => d.IsKind(SyntaxKind.ParenthesizedLambdaExpression)
|
||||
|| d.IsKind(SyntaxKind.SimpleLambdaExpression)) is LambdaExpressionSyntax existingLambda)
|
||||
{
|
||||
updatedParent = GetNodeWithUpdatedLambda(existingLambda, change, parent);
|
||||
}
|
||||
else // Add a new lambda
|
||||
{
|
||||
updatedParent = AddLambdaToParent(parent, children, change);
|
||||
}
|
||||
|
||||
return originalMethod.ReplaceNode(parent, updatedParent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given an existing lamba expression, updates the parameters and block as necessary
|
||||
/// </summary>
|
||||
/// <param name="existingLambda"></param>
|
||||
/// <param name="change"></param>
|
||||
/// <param name="parent"></param>
|
||||
/// <returns>parent with updated lambda expression</returns>
|
||||
internal static SyntaxNode GetNodeWithUpdatedLambda(LambdaExpressionSyntax existingLambda, CodeSnippet change, SyntaxNode parent)
|
||||
{
|
||||
var lambdaWithUpdatedParameters = UpdateLambdaParameters(existingLambda, change);
|
||||
var lambdaWithUpdatedBlock = UpdateLambdaBlock(lambdaWithUpdatedParameters, change);
|
||||
|
||||
return parent.ReplaceNode(existingLambda, lambdaWithUpdatedBlock);
|
||||
}
|
||||
|
||||
private static LambdaExpressionSyntax UpdateLambdaParameters(LambdaExpressionSyntax existingLambda, CodeSnippet change)
|
||||
{
|
||||
var existingParameters = GetDescendantNodes(existingLambda)?.Where(n => n.IsKind(SyntaxKind.Parameter));
|
||||
if (existingParameters is null || string.IsNullOrEmpty(change.Parameter))
|
||||
{
|
||||
return existingLambda;
|
||||
}
|
||||
|
||||
if (ProjectModifierHelper.StatementExists(existingParameters, change.Parameter))
|
||||
{
|
||||
return existingLambda;
|
||||
}
|
||||
|
||||
var lambdaParam = SyntaxFactory.Parameter(SyntaxFactory.Identifier(change.Parameter));
|
||||
// Lambda might be ParenthesizedLambda or SimpleLambda
|
||||
if (existingLambda is ParenthesizedLambdaExpressionSyntax updatedLambda)
|
||||
{
|
||||
updatedLambda = updatedLambda.AddParameterListParameters(lambdaParam);
|
||||
}
|
||||
else // if SimpleLambda we are adding a parameter and making it a ParenthesizedLambda
|
||||
{
|
||||
updatedLambda = SyntaxFactory.ParenthesizedLambdaExpression(existingLambda).AddParameterListParameters(lambdaParam);
|
||||
}
|
||||
|
||||
if (updatedLambda == null)
|
||||
{
|
||||
return existingLambda;
|
||||
}
|
||||
|
||||
return updatedLambda;
|
||||
}
|
||||
|
||||
private static LambdaExpressionSyntax UpdateLambdaBlock(LambdaExpressionSyntax existingLambda, CodeSnippet change)
|
||||
{
|
||||
if (ProjectModifierHelper.StatementExists(existingLambda.Body, change.Block))
|
||||
{
|
||||
return existingLambda;
|
||||
}
|
||||
|
||||
var leadingWhitespaceTrivia = existingLambda.GetLeadingTrivia().LastOrDefault();
|
||||
if (leadingWhitespaceTrivia.IsKind(SyntaxKind.WhitespaceTrivia))
|
||||
{
|
||||
change.LeadingTrivia.NumberOfSpaces += leadingWhitespaceTrivia.Span.Length;
|
||||
}
|
||||
|
||||
if (change.Replace)
|
||||
{
|
||||
return existingLambda.WithBody(GetStatementWithTrivia(change));
|
||||
}
|
||||
|
||||
// Get new lambda block
|
||||
var updatedBlock = GetBlockStatement(existingLambda.Body, change);
|
||||
|
||||
// Try to replace existing block with updated block
|
||||
if (existingLambda.WithBody(updatedBlock as CSharpSyntaxNode) is LambdaExpressionSyntax updatedLambda)
|
||||
{
|
||||
return updatedLambda;
|
||||
}
|
||||
|
||||
return existingLambda;
|
||||
}
|
||||
|
||||
internal static SyntaxNode AddLambdaToParent(SyntaxNode parent, IEnumerable<SyntaxNode> children, CodeSnippet change)
|
||||
{
|
||||
if (ProjectModifierHelper.StatementExists(children, change.Block) ||
|
||||
string.IsNullOrEmpty(change.Parameter) ||
|
||||
// Determine if there is an existing argument list to add the lambda
|
||||
!(children.FirstOrDefault(n => n.IsKind(SyntaxKind.ArgumentList)) is ArgumentListSyntax argList))
|
||||
{
|
||||
return parent;
|
||||
}
|
||||
|
||||
// Create a lambda parameter
|
||||
var parameter = SyntaxFactory.Parameter(
|
||||
SyntaxFactory.Identifier(change.Parameter))
|
||||
.WithTrailingTrivia(SyntaxFactory.Space);
|
||||
|
||||
var parentLeadingWhiteSpace = parent.GetLeadingTrivia().LastOrDefault();
|
||||
if (parentLeadingWhiteSpace.IsKind(SyntaxKind.WhitespaceTrivia))
|
||||
{
|
||||
change.LeadingTrivia.NumberOfSpaces += parentLeadingWhiteSpace.Span.Length;
|
||||
}
|
||||
|
||||
// Ensure that block statement is valid
|
||||
if (!(GetBlockStatement(SyntaxFactory.Block(), change) is BlockSyntax block))
|
||||
{
|
||||
return parent;
|
||||
}
|
||||
|
||||
// Update white space for non-top-level statements
|
||||
if (parentLeadingWhiteSpace.IsKind(SyntaxKind.WhitespaceTrivia))
|
||||
{
|
||||
block = block
|
||||
.WithOpenBraceToken(block.OpenBraceToken.WithLeadingTrivia(parentLeadingWhiteSpace))
|
||||
.WithCloseBraceToken(block.CloseBraceToken.WithLeadingTrivia(parentLeadingWhiteSpace));
|
||||
}
|
||||
|
||||
// Create lambda expression with parameter and block (add leading newline to block for formatting)
|
||||
var newLambdaExpression = SyntaxFactory.SimpleLambdaExpression(
|
||||
parameter,
|
||||
block.WithLeadingTrivia(parentLeadingWhiteSpace));
|
||||
|
||||
// Add lambda to parent block's argument list
|
||||
var argument = SyntaxFactory.Argument(newLambdaExpression);
|
||||
var updatedParent = parent.ReplaceNode(argList, argList.AddArguments(argument));
|
||||
|
||||
return updatedParent;
|
||||
}
|
||||
|
||||
// return modified parent node with code snippet added
|
||||
internal static SyntaxNode AddExpressionToParent(SyntaxNode originalMethod, CodeSnippet change)
|
||||
{
|
||||
// Determine the parent node onto which we are adding
|
||||
var descendantNodes = GetDescendantNodes(originalMethod);
|
||||
if (descendantNodes is null)
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
var parent = GetSpecifiedNode(change.Parent, descendantNodes);
|
||||
if (parent is null)
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
var children = GetDescendantNodes(parent);
|
||||
if (children is null)
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
if (ProjectModifierHelper.StatementExists(children, change.Block))
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
// Find parent's expression statement
|
||||
if (!(children.FirstOrDefault(n => n.IsKind(SyntaxKind.ExpressionStatement)) is ExpressionStatementSyntax exprNode))
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
// Create new expression to update old expression
|
||||
var leadingTrivia = GetLeadingTrivia(change.LeadingTrivia);
|
||||
var identifier = SyntaxFactory.IdentifierName(change.Block);
|
||||
|
||||
var newExpression = SyntaxFactory.MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
exprNode.Expression.WithTrailingTrivia(leadingTrivia),
|
||||
identifier);
|
||||
|
||||
var modifiedExprNode = exprNode.WithExpression(newExpression);
|
||||
|
||||
if (modifiedExprNode is null)
|
||||
{
|
||||
return originalMethod;
|
||||
}
|
||||
|
||||
// Replace existing expression with updated expression
|
||||
var updatedParent = parent.ReplaceNode(exprNode, modifiedExprNode);
|
||||
|
||||
return updatedParent != null ? originalMethod.ReplaceNode(parent, updatedParent) : originalMethod;
|
||||
}
|
||||
|
||||
internal static IEnumerable<SyntaxNode>? GetDescendantNodes(SyntaxNode root)
|
||||
{
|
||||
if (root is BlockSyntax block)
|
||||
{
|
||||
return block.Statements;
|
||||
}
|
||||
else if (root is CompilationUnitSyntax compilationUnit)
|
||||
{
|
||||
return compilationUnit.Members;
|
||||
}
|
||||
|
||||
return root?.DescendantNodes();
|
||||
}
|
||||
|
||||
// create UsingDirectiveSyntax[] using a string[] to add to the root of the class (root.Usings).
|
||||
internal static UsingDirectiveSyntax[] CreateUsings(string[]? usings)
|
||||
{
|
||||
var usingDirectiveList = new List<UsingDirectiveSyntax>();
|
||||
if (usings == null)
|
||||
{
|
||||
return usingDirectiveList.ToArray();
|
||||
}
|
||||
|
||||
var nameLeadingTrivia = SyntaxFactory.TriviaList(SyntaxFactory.Space);
|
||||
var usingTrailingTrivia = SyntaxFactory.TriviaList(SyntaxFactory.CarriageReturnLineFeed);
|
||||
foreach (var usingDirectiveString in usings)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(usingDirectiveString))
|
||||
{
|
||||
//leading space on the value of the using eg. (using' 'Microsoft.Yadada)
|
||||
var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName(usingDirectiveString)
|
||||
.WithLeadingTrivia(nameLeadingTrivia))
|
||||
.WithAdditionalAnnotations(Formatter.Annotation)
|
||||
.WithTrailingTrivia(usingTrailingTrivia);
|
||||
|
||||
usingDirectiveList.Add(usingDirective);
|
||||
}
|
||||
}
|
||||
|
||||
return usingDirectiveList.ToArray();
|
||||
}
|
||||
|
||||
// create AttributeListSyntax using string[] to add on top of a ClassDeclrationSyntax
|
||||
internal static SyntaxList<AttributeListSyntax> CreateAttributeList(CodeBlock[] attributes, SyntaxList<AttributeListSyntax> attributeLists, SyntaxTriviaList leadingTrivia)
|
||||
{
|
||||
if (attributes == null)
|
||||
{
|
||||
return attributeLists;
|
||||
}
|
||||
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
var attributeList = new List<AttributeSyntax>();
|
||||
// filter by apps
|
||||
if (!string.IsNullOrEmpty(attribute.Block) && !ProjectModifierHelper.AttributeExists(attribute.Block, attributeLists))
|
||||
{
|
||||
attributeList.Add(SyntaxFactory.Attribute(SyntaxFactory.ParseName(attribute.Block)));
|
||||
}
|
||||
|
||||
if (attributeList.Any())
|
||||
{
|
||||
var attributeListSyntax = SyntaxFactory.AttributeList(SyntaxFactory.SeparatedList(attributeList)).WithLeadingTrivia(leadingTrivia);
|
||||
if (!leadingTrivia.ToString().Contains("\n"))
|
||||
{
|
||||
attributeListSyntax = attributeListSyntax.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);
|
||||
}
|
||||
|
||||
attributeLists = attributeLists.Insert(0, attributeListSyntax);
|
||||
}
|
||||
}
|
||||
|
||||
return attributeLists;
|
||||
}
|
||||
|
||||
//create MemberDeclarationSyntax[] to add at the top of ClassDeclarationSynax. Created from the property strings.
|
||||
internal MemberDeclarationSyntax[] CreateClassProperties(
|
||||
SyntaxList<MemberDeclarationSyntax> members,
|
||||
SyntaxTriviaList leadingTrivia,
|
||||
SyntaxTriviaList trailingTrivia)
|
||||
{
|
||||
var propertyDeclarationList = new List<MemberDeclarationSyntax>();
|
||||
if (_codeFile.ClassProperties != null && _codeFile.ClassProperties.Any())
|
||||
{
|
||||
foreach (var classProperty in _codeFile.ClassProperties)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(classProperty.Block) && !PropertyExists(classProperty.Block, members))
|
||||
{
|
||||
var additionalAnnotation = Formatter.Annotation;
|
||||
var classPropertyDeclaration = SyntaxFactory.ParseMemberDeclaration(classProperty.Block)
|
||||
?.WithAdditionalAnnotations(additionalAnnotation)
|
||||
?.WithTrailingTrivia(trailingTrivia)
|
||||
?.WithLeadingTrivia(leadingTrivia);
|
||||
|
||||
if (classPropertyDeclaration != null)
|
||||
{
|
||||
propertyDeclarationList.Add(classPropertyDeclaration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return propertyDeclarationList.ToArray();
|
||||
}
|
||||
|
||||
internal static bool PropertyExists(string property, SyntaxList<MemberDeclarationSyntax> members)
|
||||
{
|
||||
if (string.IsNullOrEmpty(property))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var trimmedProperty = property.Trim(ProjectModifierHelper.CodeSnippetTrimChars);
|
||||
|
||||
return members.Where(m => m.ToString().Trim(ProjectModifierHelper.CodeSnippetTrimChars).Equals(trimmedProperty)).Any();
|
||||
}
|
||||
|
||||
private static SyntaxTrivia SemiColonTrivia => SyntaxFactory.Trivia(SyntaxFactory.SkippedTokensTrivia()
|
||||
.WithTokens(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.SemicolonToken))));
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn;
|
||||
|
||||
public static class EfDbContextHelpers
|
||||
{
|
||||
public static EfDbContextProperties GetEfDbContextProperties(ISymbol dbContextSymbol, ISymbol modelSymbol)
|
||||
{
|
||||
EfDbContextProperties efDbContextProperties = new();
|
||||
if (dbContextSymbol is INamedTypeSymbol dbContextTypeSymbol)
|
||||
{
|
||||
// Assuming there's only one DbSet property in the DbContext for simplicity
|
||||
var dbSetProperties = dbContextTypeSymbol.GetMembers().OfType<IPropertySymbol>()
|
||||
.Where(p => p.Type is INamedTypeSymbol namedTypeSymbol &&
|
||||
namedTypeSymbol.Interfaces.Any(i => i.ToDisplayString() == "Microsoft.EntityFrameworkCore.DbSet"));
|
||||
|
||||
if (dbSetProperties.Any())
|
||||
{
|
||||
var dbSetProperty = (INamedTypeSymbol)dbSetProperties.First();
|
||||
var entityType = (INamedTypeSymbol)dbSetProperties.First().Type;
|
||||
var primaryKey = entityType.GetMembers().OfType<IPropertySymbol>().FirstOrDefault(IsKeyProperty);
|
||||
if (primaryKey != null)
|
||||
{
|
||||
efDbContextProperties.PrimaryKeyName = primaryKey.Name;
|
||||
efDbContextProperties.PrimaryKeyShortTypeName = primaryKey.Type.Name;
|
||||
efDbContextProperties.PrimaryKeyTypeName = primaryKey.Type.ToDisplayString();
|
||||
efDbContextProperties.EntitySetName = $"{dbSetProperty.Name}";
|
||||
}
|
||||
}
|
||||
|
||||
efDbContextProperties.ModelProperties = GetModelProperties(modelSymbol);
|
||||
}
|
||||
|
||||
return efDbContextProperties;
|
||||
}
|
||||
|
||||
private static List<IPropertySymbol> GetModelProperties(ISymbol modelSymbol)
|
||||
{
|
||||
List<IPropertySymbol> properties = [];
|
||||
|
||||
if (modelSymbol is INamedTypeSymbol namedTypeSymbol)
|
||||
{
|
||||
properties.AddRange(namedTypeSymbol.GetMembers().OfType<IPropertySymbol>());
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static bool IsKeyProperty(IPropertySymbol propertySymbol)
|
||||
{
|
||||
return propertySymbol.GetAttributes().Any(a => a.AttributeClass?.Name == "KeyAttribute");
|
||||
}
|
||||
}
|
||||
|
||||
public class EfDbContextProperties
|
||||
{
|
||||
public List<IPropertySymbol> ModelProperties { get; set; } = default!;
|
||||
public string PrimaryKeyName { get; set; } = default!;
|
||||
public string PrimaryKeyShortTypeName { get; set; } = default!;
|
||||
public string PrimaryKeyTypeName { get; set; } = default!;
|
||||
public string EntitySetName { get; set; } = default!;
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Extensions.Roslyn;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn;
|
||||
|
||||
public class ProjectModifier
|
||||
{
|
||||
private readonly ILogger _consoleLogger;
|
||||
private readonly ICodeService _codeService;
|
||||
private readonly IAppSettings _appSettings;
|
||||
private const string Main = nameof(Main);
|
||||
private readonly StringBuilder _output;
|
||||
private readonly CodeChangeOptions _codeChangeOptions;
|
||||
private readonly CodeModifierConfig? _codeModifierConfig;
|
||||
|
||||
public ProjectModifier(
|
||||
IAppSettings appSettings,
|
||||
ICodeService codeService,
|
||||
ILogger consoleLogger,
|
||||
CodeChangeOptions codeChangeOptions,
|
||||
CodeModifierConfig? codeModifierConfig = null)
|
||||
{
|
||||
_appSettings = appSettings;
|
||||
_codeService = codeService;
|
||||
_consoleLogger = consoleLogger ?? throw new ArgumentNullException(nameof(consoleLogger));
|
||||
_output = new StringBuilder();
|
||||
_codeChangeOptions = codeChangeOptions;
|
||||
_codeModifierConfig = codeModifierConfig;
|
||||
}
|
||||
|
||||
public async Task<bool> RunAsync()
|
||||
{
|
||||
if (_codeModifierConfig is null || _codeModifierConfig.Files is null || !_codeModifierConfig.Files.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var solution = (await _codeService.GetWorkspaceAsync())?.CurrentSolution;
|
||||
var roslynProject = solution?.GetProject(_appSettings.Workspace().InputPath);
|
||||
|
||||
var filteredFiles = _codeModifierConfig.Files.Where(f => ProjectModifierHelper.FilterOptions(f.Options, _codeChangeOptions));
|
||||
foreach (var file in filteredFiles)
|
||||
{
|
||||
Document? originalDocument = roslynProject?.GetDocument(file.FileName);
|
||||
var modifiedDocument = await HandleCodeFileAsync(originalDocument, file, _codeChangeOptions);
|
||||
roslynProject = modifiedDocument?.Project;
|
||||
}
|
||||
|
||||
return _codeService.TryApplyChanges(roslynProject?.Solution);
|
||||
}
|
||||
|
||||
private async Task<Document?> HandleCodeFileAsync(Document? document, CodeFile file, CodeChangeOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(file.AddFilePath))
|
||||
{
|
||||
return AddFile(file);
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (file.Extension)
|
||||
{
|
||||
case "cs":
|
||||
return await ModifyCsFile(file, document, options);
|
||||
case "cshtml":
|
||||
return await ModifyCshtmlFile(file, document, options);
|
||||
case "razor":
|
||||
case "html":
|
||||
return await ApplyTextReplacements(file, document, options);
|
||||
}
|
||||
|
||||
//_output.AppendLine(string.Format(Resources.ModifiedCodeFile, file.FileName));
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
//_output.Append(string.Format(Resources.FailedToModifyCodeFile, file.FileName, e.Message));
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
internal static async Task<Document?> ModifyCshtmlFile(CodeFile file, Document? fileDoc, CodeChangeOptions options)
|
||||
{
|
||||
if (fileDoc is null || file.Methods is null || !file.Methods.TryGetValue("Global", out var globalMethod))
|
||||
{
|
||||
return fileDoc;
|
||||
}
|
||||
|
||||
var filteredCodeChanges = globalMethod?.CodeChanges?.Where(cc => ProjectModifierHelper.FilterOptions(cc.Options, options));
|
||||
if (filteredCodeChanges != null && !filteredCodeChanges.Any())
|
||||
{
|
||||
return fileDoc;
|
||||
}
|
||||
|
||||
// add code snippets/changes.
|
||||
return await ProjectModifierHelper.ModifyDocumentTextAsync(fileDoc, filteredCodeChanges);
|
||||
/* if (editedDocument != null)
|
||||
{
|
||||
await ProjectModifierHelper.UpdateDocument(editedDocument);
|
||||
}*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates .razor and .html files via string replacement
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <param name="project"></param>
|
||||
/// <param name="toolOptions"></param>
|
||||
/// <returns></returns>
|
||||
internal static async Task<Document?> ApplyTextReplacements(CodeFile file, Document? document, CodeChangeOptions toolOptions)
|
||||
{
|
||||
if (document is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var replacements = file.Replacements?.Where(cc => ProjectModifierHelper.FilterOptions(cc.Options, toolOptions));
|
||||
if (replacements != null && !replacements.Any())
|
||||
{
|
||||
return document;
|
||||
}
|
||||
|
||||
return await ProjectModifierHelper.ModifyDocumentTextAsync(document, replacements);
|
||||
/* if (editedDocument != null)
|
||||
{
|
||||
await ProjectModifierHelper.UpdateDocument(editedDocument);
|
||||
}*/
|
||||
}
|
||||
|
||||
internal async Task<Document?> ModifyCsFile(CodeFile file, Document? fileDoc, CodeChangeOptions options)
|
||||
{
|
||||
if (fileDoc is null || string.IsNullOrEmpty(fileDoc.Name))
|
||||
{
|
||||
return fileDoc;
|
||||
}
|
||||
|
||||
DocumentBuilder documentBuilder = new(fileDoc, file, _consoleLogger);
|
||||
return await documentBuilder.RunAsync();
|
||||
}
|
||||
|
||||
private Document? AddFile(CodeFile file)
|
||||
{
|
||||
/* var filePath = Path.Combine("TODOprojectpath", file.AddFilePath);
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
return; // File exists, don't need to create
|
||||
}
|
||||
|
||||
var codeFileString = string.Empty;// GetCodeFileString(file);
|
||||
|
||||
var fileDir = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrEmpty(fileDir))
|
||||
{
|
||||
Directory.CreateDirectory(fileDir);
|
||||
File.WriteAllText(filePath, codeFileString);
|
||||
}*/
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,615 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Extensions;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.General;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Roslyn;
|
||||
|
||||
public static class ProjectModifierHelper
|
||||
{
|
||||
internal static char[] CodeSnippetTrimChars = new char[] { ' ', '\r', '\n', ';' };
|
||||
internal static IEnumerable<string> CodeSnippetTrimStrings = CodeSnippetTrimChars.Select(c => c.ToString());
|
||||
internal static char[] Parentheses = new char[] { '(', ')' };
|
||||
internal const string VarIdentifier = "var";
|
||||
internal const string WebApplicationBuilderIdentifier = "WebApplicationBuilder";
|
||||
|
||||
public const string Main = nameof(Main);
|
||||
|
||||
public static CodeModifierConfig? GetCodeModifierConfig(string configName, Assembly? assembly = null)
|
||||
{
|
||||
assembly = assembly ?? Assembly.GetExecutingAssembly();
|
||||
string jsonText = ProjectModelHelper.GetManifestResource(assembly, shortResourceName: configName);
|
||||
return System.Text.Json.JsonSerializer.Deserialize<CodeModifierConfig>(jsonText);
|
||||
}
|
||||
/// <summary>
|
||||
/// Check if Startup.cs or similar file exists.
|
||||
/// </summary>
|
||||
/// <returns>true if Startup.cs does not exist, false if it does exist.</returns>
|
||||
public static async Task<bool> IsMinimalApp(ICodeService codeService)
|
||||
{
|
||||
//find Startup if named Startup.
|
||||
var allClassSymbols = await codeService.GetAllClassSymbolsAsync();
|
||||
var startupType = allClassSymbols.FirstOrDefault(x => x.Name.Equals("Startup", StringComparison.OrdinalIgnoreCase));
|
||||
if (startupType == null)
|
||||
{
|
||||
//if changed the name in Program.cs, get the class name and check.
|
||||
var programDocument = (await codeService.GetAllDocumentsAsync()).FirstOrDefault(d => d.Name.EndsWith("Program.cs"));
|
||||
var startupClassName = await GetStartupClassName(programDocument);
|
||||
startupType = allClassSymbols.FirstOrDefault(x => x.Name.Equals(startupClassName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
return startupType == null;
|
||||
}
|
||||
|
||||
public static async Task<bool> IsUsingTopLevelStatements(ICodeService codeService)
|
||||
{
|
||||
var allDocuments = await codeService.GetAllDocumentsAsync();
|
||||
var programDocument = allDocuments?.FirstOrDefault(d => d.Name.EndsWith("Program.cs"));
|
||||
if (programDocument != null && await programDocument.GetSyntaxRootAsync() is CompilationUnitSyntax root)
|
||||
{
|
||||
var fileScopedNamespaceNode = root.Members.OfType<FileScopedNamespaceDeclarationSyntax>()?.FirstOrDefault();
|
||||
if (fileScopedNamespaceNode == null)
|
||||
{
|
||||
var mainMethod = DocumentBuilder.GetMethodFromSyntaxRoot(root, Main);
|
||||
return mainMethod == null;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the csproj xml text and gets one or more TargetFrameworks for the project.
|
||||
/// </summary>
|
||||
/// <param name="csprojText">.csproj file as string</param>
|
||||
/// <returns>string[] containing target frameworks of the project</returns>
|
||||
internal static string[] ProcessCsprojTfms(string csprojText)
|
||||
{
|
||||
List<string> processedTfms = new List<string>();
|
||||
if (!string.IsNullOrEmpty(csprojText))
|
||||
{
|
||||
//use XDocument to get all csproj elements.
|
||||
XDocument document = XDocument.Parse(csprojText);
|
||||
var docNodes = document.Root?.Elements();
|
||||
var allElements = docNodes?.SelectMany(x => x.Elements());
|
||||
//add them to a dictionary for easy comparisons.
|
||||
var csprojVariables = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (allElements != null && allElements.Any())
|
||||
{
|
||||
foreach (var elem in allElements)
|
||||
{
|
||||
//dont' add PackageReference(s) since they are useless for getting tfm properties.
|
||||
if (!elem.Name.LocalName.Equals("PackageReference", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
//change the keys from TargetFramework to $(TargetFramework) and so forth for nested variable analysis.
|
||||
//eg. analysing <TargetFramework>$(X)</TargetFramework> and getting the value for $(X).
|
||||
//makes for a easy string comparison without using regex and splitting.
|
||||
string tfmKey = string.Format("$({0})", elem.Name.LocalName);
|
||||
if (!csprojVariables.ContainsKey(tfmKey))
|
||||
{
|
||||
csprojVariables.Add(tfmKey, elem.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//if only one TargetFramework
|
||||
if (csprojVariables.TryGetValue("$(TargetFramework)", out string? tfmValue))
|
||||
{
|
||||
string processedTfm = ProcessTfm(tfmValue.Trim(), csprojVariables);
|
||||
if (!string.IsNullOrEmpty(processedTfm) && ProjectModelHelper.ShortTfmDictionary.Values.ToList().Contains(processedTfm, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
processedTfms.Add(processedTfm);
|
||||
}
|
||||
}
|
||||
//if multiple, split by ';' and add them all.
|
||||
else if (csprojVariables.TryGetValue("$(TargetFrameworks)", out string? tfms))
|
||||
{
|
||||
string processedTfm = ProcessTfm(tfms.Trim(), csprojVariables);
|
||||
//tfms should be separated by ;
|
||||
var splitTfms = processedTfm.Split(';');
|
||||
foreach (var tfm in splitTfms)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(tfm) && ProjectModelHelper.ShortTfmDictionary.Values.ToList().Contains(tfm, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
processedTfms.Add(tfm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return processedTfms.ToArray();
|
||||
}
|
||||
|
||||
// Returns true when there is no Startup.cs or equivalent
|
||||
internal static async Task<bool> IsMinimalApp(List<Document> documents)
|
||||
{
|
||||
if (documents.Where(d => d.Name.EndsWith("Startup.cs")).Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// if changed the name in Program.cs, get the class name and check.
|
||||
var programDocument = documents.FirstOrDefault(d => d.Name.EndsWith("Program.cs"));
|
||||
var startupClassName = await GetStartupClassName(programDocument);
|
||||
|
||||
return string.IsNullOrEmpty(startupClassName); // If project has UseStartup in Program.cs, it is not a minimal app
|
||||
}
|
||||
|
||||
// Get Startup class name from CreateHostBuilder in Program.cs. If Program.cs is not being used, method
|
||||
// will return null.
|
||||
internal static async Task<string> GetStartupClass(List<Document> documents)
|
||||
{
|
||||
string startupClassName = string.Empty;
|
||||
if (documents != null && documents.Any())
|
||||
{
|
||||
var programCsDocument = documents.FirstOrDefault(d => d.Name.Equals("Program.cs"));
|
||||
startupClassName = await GetStartupClassName(programCsDocument);
|
||||
}
|
||||
|
||||
return string.IsNullOrEmpty(startupClassName) ? string.Empty : string.Concat(startupClassName, ".cs");
|
||||
}
|
||||
|
||||
internal static async Task<string> GetStartupClassName(Document? programDoc)
|
||||
{
|
||||
if (programDoc != null && await programDoc.GetSyntaxRootAsync() is CompilationUnitSyntax root)
|
||||
{
|
||||
var namespaceNode = root.Members.OfType<NamespaceDeclarationSyntax>()?.FirstOrDefault();
|
||||
var programClassNode =
|
||||
namespaceNode?.DescendantNodes()
|
||||
.FirstOrDefault(node =>
|
||||
node is ClassDeclarationSyntax cds &&
|
||||
cds.Identifier
|
||||
.ValueText.Contains("Program")) ??
|
||||
root?.DescendantNodes()
|
||||
.FirstOrDefault(node =>
|
||||
node is ClassDeclarationSyntax cds &&
|
||||
cds.Identifier
|
||||
.ValueText.Contains("Program"));
|
||||
|
||||
var useStartupNode = programClassNode?.DescendantNodes()?
|
||||
.FirstOrDefault(node =>
|
||||
node is MemberAccessExpressionSyntax maes &&
|
||||
maes.ToString()
|
||||
.Contains("webBuilder.UseStartup"));
|
||||
|
||||
var useStartupTxt = useStartupNode?.ToString();
|
||||
if (!string.IsNullOrEmpty(useStartupTxt))
|
||||
{
|
||||
int startIndex = useStartupTxt.IndexOf("<");
|
||||
int endIndex = useStartupTxt.IndexOf(">");
|
||||
if (startIndex > -1 && endIndex > startIndex)
|
||||
{
|
||||
return useStartupTxt.Substring(startIndex + 1, endIndex - startIndex - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
internal static string GetClassName(string className)
|
||||
{
|
||||
string formattedClassName = string.Empty;
|
||||
if (!string.IsNullOrEmpty(className))
|
||||
{
|
||||
//switched to using string[] for netstandard2.0 compatibility.
|
||||
string[] blocks = className.Split(new string[] { ".cs" }, StringSplitOptions.None);
|
||||
if (blocks.Length > 1)
|
||||
{
|
||||
return blocks[0];
|
||||
}
|
||||
}
|
||||
return formattedClassName;
|
||||
}
|
||||
|
||||
internal static BaseMethodDeclarationSyntax? GetOriginalMethod(ClassDeclarationSyntax classNode, string methodName, Method methodChanges)
|
||||
{
|
||||
if (classNode?.Members.FirstOrDefault(node
|
||||
=> node is MethodDeclarationSyntax mds && mds.Identifier.ValueText.Equals(methodName)
|
||||
|| node is ConstructorDeclarationSyntax cds && cds.Identifier.ValueText.Equals(methodName))
|
||||
is BaseMethodDeclarationSyntax foundMethod)
|
||||
{
|
||||
return foundMethod;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// check if the parameters match for the given method, and populate a Dictionary with parameter.Type keys and Parameter.Identifier values.
|
||||
internal static IDictionary<string, string> VerifyParameters(string[] parametersToCheck, List<ParameterSyntax> foundParameters)
|
||||
{
|
||||
IDictionary<string, string> parametersWithNames = new Dictionary<string, string>();
|
||||
if (foundParameters.Any() && parametersToCheck != null && parametersToCheck.Any())
|
||||
{
|
||||
var pars = foundParameters.ToList();
|
||||
foreach (var parameter in parametersToCheck)
|
||||
{
|
||||
//Trim(' ') for the additional whitespace at the end of the parameter.Type string. Parameter.Type should be a singular word.
|
||||
var verifiedParams = pars.Where(p => (p.Type?.ToFullString()?.Trim(' ')?.Equals(parameter)).GetValueOrDefault(false));
|
||||
if (verifiedParams.Any())
|
||||
{
|
||||
parametersWithNames.Add(parameter, verifiedParams.First().Identifier.ValueText);
|
||||
}
|
||||
}
|
||||
|
||||
//Dictionary should have the same number of parameters we are trying to verify.
|
||||
if (parametersWithNames.Count == parametersToCheck.Length)
|
||||
{
|
||||
return parametersWithNames;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
|
||||
return parametersWithNames;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format a string of a SimpleMemberAccessExpression(eg., Type.Value)
|
||||
/// Replace Type with its value from the parameterDict.
|
||||
/// </summary>
|
||||
/// <param name="codeBlock">SimpleMemberAccessExpression string</param>
|
||||
/// <param name="parameterDict">IDictionary with parameter type keys and values</param>
|
||||
/// <param name="trim">Whether to trim the resulting string</param>
|
||||
/// <returns></returns>
|
||||
internal static string FormatCodeBlock(string codeBlock, IDictionary<string, string> parameterDict, bool trim = false)
|
||||
{
|
||||
string formattedCodeBlock = codeBlock;
|
||||
if (!string.IsNullOrEmpty(codeBlock) && parameterDict != null)
|
||||
{
|
||||
string value = Regex.Replace(codeBlock, "^([^.]*).", "");
|
||||
string param = Regex.Replace(codeBlock, "[*^.].*", "");
|
||||
if (parameterDict.TryGetValue(param, out string? parameter))
|
||||
{
|
||||
formattedCodeBlock = $"{parameter}.{value}";
|
||||
}
|
||||
}
|
||||
|
||||
return trim ? formattedCodeBlock.Trim(CodeSnippetTrimChars) : formattedCodeBlock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks for oldValue in codeBlock and replaces with newValue
|
||||
/// </summary>
|
||||
/// <param name="codeBlock"></param>
|
||||
/// <param name="oldValue"></param>
|
||||
/// <param name="newValue"></param>
|
||||
/// <returns>codeBlock where any instance of 'oldValue' is replaced with 'newValue'</returns>
|
||||
internal static string ReplaceValue(string codeBlock, string oldValue, string newValue)
|
||||
{
|
||||
string formattedStatement = codeBlock;
|
||||
if (!string.IsNullOrEmpty(formattedStatement) && !string.IsNullOrEmpty(oldValue) && !string.IsNullOrEmpty(newValue))
|
||||
{
|
||||
return formattedStatement.Replace(oldValue, newValue);
|
||||
}
|
||||
|
||||
return formattedStatement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces all instances of the old value with the new value
|
||||
/// </summary>
|
||||
/// <param name="changes"></param>
|
||||
/// <param name="oldValue"></param>
|
||||
/// <param name="newValue"></param>
|
||||
/// <returns>updated CodeSnippet array</returns>
|
||||
internal static CodeSnippet[]? UpdateVariables(CodeSnippet[]? changes, string oldValue, string newValue) => changes?.Select(c => UpdateVariables(c, oldValue, newValue)).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Replaces all instances of the old value with the new value
|
||||
/// </summary>
|
||||
/// <param name="change"></param>
|
||||
/// <param name="oldValue"></param>
|
||||
/// <param name="newValue"></param>
|
||||
/// <returns>updated CodeSnippet</returns>
|
||||
internal static CodeSnippet UpdateVariables(CodeSnippet change, string oldValue, string newValue) //TODO
|
||||
{
|
||||
// format CodeSnippet fields for any variables or parameters.
|
||||
if (!string.IsNullOrEmpty(change.Block))
|
||||
{
|
||||
change.Block = ReplaceValue(change.Block, oldValue, newValue);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(change.Parent))
|
||||
{
|
||||
change.Parent = ReplaceValue(change.Parent, oldValue, newValue);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(change.CheckBlock))
|
||||
{
|
||||
change.CheckBlock = ReplaceValue(change.CheckBlock, oldValue, newValue);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(change.InsertAfter))
|
||||
{
|
||||
change.InsertAfter = ReplaceValue(change.InsertAfter, oldValue, newValue);
|
||||
}
|
||||
if (change.InsertBefore != null && change.InsertBefore.Any())
|
||||
{
|
||||
for (int i = 0; i < change.InsertBefore.Count(); i++)
|
||||
{
|
||||
change.InsertBefore[i] = ReplaceValue(change.InsertBefore[i], oldValue, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trim ' ', '\r', '\n' and replace any whitespace with no spaces.
|
||||
/// </summary>
|
||||
/// <param name="statement"></param>
|
||||
/// <returns></returns>
|
||||
internal static string TrimStatement(string statement)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(statement);
|
||||
if (!string.IsNullOrEmpty(statement))
|
||||
{
|
||||
foreach (string replacement in CodeSnippetTrimStrings)
|
||||
{
|
||||
sb.Replace(replacement, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches through the list of nodes to find replacement variable identifier names
|
||||
/// </summary>
|
||||
/// <param name="members"></param>
|
||||
/// <returns></returns>
|
||||
internal static (string, string)? GetBuilderVariableIdentifierTransformation(SyntaxList<MemberDeclarationSyntax> members)
|
||||
{
|
||||
if (!(members.FirstOrDefault(
|
||||
m => TrimStatement(m.ToString())
|
||||
.Contains("=WebApplication.CreateBuilder")) is SyntaxNode memberNode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var memberString = TrimStatement(memberNode.ToString());
|
||||
|
||||
var start = 0;
|
||||
if (memberString.Contains(VarIdentifier))
|
||||
{
|
||||
start = memberString.IndexOf(VarIdentifier) + VarIdentifier.Length;
|
||||
}
|
||||
else if (memberString.Contains(WebApplicationBuilderIdentifier))
|
||||
{
|
||||
start = memberString.IndexOf(WebApplicationBuilderIdentifier) + WebApplicationBuilderIdentifier.Length;
|
||||
}
|
||||
if (start > 0)
|
||||
{
|
||||
var end = memberString.IndexOf("=");
|
||||
return ("WebApplication.CreateBuilder", memberString.Substring(start, end - start));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static bool GlobalStatementExists(CompilationUnitSyntax root, GlobalStatementSyntax statement)
|
||||
{
|
||||
if (root != null && statement != null)
|
||||
{
|
||||
var formattedStatementString = TrimStatement(statement.ToString());
|
||||
bool foundStatement = root.Members.Where(st => TrimStatement(st.ToString()).Contains(formattedStatementString)).Any();
|
||||
|
||||
if (foundStatement)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool AttributeExists(string attribute, SyntaxList<AttributeListSyntax> attributeList)
|
||||
{
|
||||
if (attributeList.Any() && !string.IsNullOrEmpty(attribute))
|
||||
{
|
||||
return attributeList.Where(al => al.Attributes.Where(attr => attr.ToString().Equals(attribute, StringComparison.OrdinalIgnoreCase)).Any()).Any();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool StatementExists(IEnumerable<SyntaxNode> nodes, string statement)
|
||||
{
|
||||
statement = statement.TrimEnd(Parentheses);
|
||||
return nodes.Any(n => StatementExists(n, statement));
|
||||
}
|
||||
|
||||
internal static bool StatementExists(SyntaxNode node, string statement)
|
||||
{
|
||||
var existingBlock = TrimStatement(node.ToString());
|
||||
var statementToCheck = TrimStatement(statement);
|
||||
|
||||
return existingBlock.Contains(statementToCheck);
|
||||
}
|
||||
|
||||
// Filter out CodeSnippets that are invalid using FilterOptions
|
||||
internal static CodeSnippet[]? FilterCodeSnippets(CodeSnippet[]? codeSnippets, CodeChangeOptions options) => codeSnippets?.Where(cs => FilterOptions(cs.Options, options)).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Filter Options string array to matching CodeChangeOptions.
|
||||
/// Primary use to filter out CodeBlocks and Files that apply in Microsoft Graph and Downstream API scenarios
|
||||
/// </summary>
|
||||
/// <param name="options">string [] in cm_*.json files for code modifications</param>
|
||||
/// <param name="codeChangeOptions">based on cli parameters</param>
|
||||
/// <returns>true if the CodeChangeOptions apply, false otherwise. </returns>
|
||||
internal static bool FilterOptions(string[]? options, CodeChangeOptions codeChangeOptions)
|
||||
{
|
||||
//if no options are passed, CodeBlock is valid
|
||||
if (options == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//if options have a "Skip", every CodeBlock is invalid
|
||||
if (options.Contains(CodeChangeOptionStrings.Skip))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// for example, program.cs is only modified when codeChangeOptions.IsMinimalApp is true
|
||||
if (options.Contains(CodeChangeOptionStrings.MinimalApp) && !codeChangeOptions.IsMinimalApp)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
//if its a minimal app and options have a "NonMinimalApp", don't add the CodeBlock
|
||||
if (options.Contains(CodeChangeOptionStrings.NonMinimalApp) && codeChangeOptions.IsMinimalApp)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//an app will either support DownstreamApi, MicrosoftGraph, both, or neither.
|
||||
if (codeChangeOptions.DownstreamApi)
|
||||
{
|
||||
if (options.Contains(CodeChangeOptionStrings.DownstreamApi) ||
|
||||
!options.Contains(CodeChangeOptionStrings.MicrosoftGraph))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (codeChangeOptions.MicrosoftGraph)
|
||||
{
|
||||
if (options.Contains(CodeChangeOptionStrings.MicrosoftGraph) ||
|
||||
!options.Contains(CodeChangeOptionStrings.DownstreamApi))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!codeChangeOptions.DownstreamApi && !codeChangeOptions.MicrosoftGraph)
|
||||
{
|
||||
if (options == null ||
|
||||
(!options.Contains(CodeChangeOptionStrings.MicrosoftGraph) &&
|
||||
!options.Contains(CodeChangeOptionStrings.DownstreamApi)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces text within document or appends text to the end of the document
|
||||
/// depending on whether change.ReplaceSnippet is set
|
||||
/// </summary>
|
||||
/// <param name="fileDoc"></param>
|
||||
/// <param name="codeChanges"></param>
|
||||
/// <returns>updated document, or null if no changes made</returns>
|
||||
internal static async Task<Document?> ModifyDocumentTextAsync(Document? fileDoc, IEnumerable<CodeSnippet>? codeChanges)
|
||||
{
|
||||
if (fileDoc is null || codeChanges is null || !codeChanges.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var sourceText = await fileDoc.GetTextAsync();
|
||||
var sourceFileString = sourceText?.ToString() ?? null;
|
||||
if (sourceFileString is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var trimmedSourceFile = TrimStatement(sourceFileString);
|
||||
var applicableCodeChanges = codeChanges.Where(c => !trimmedSourceFile.Contains(TrimStatement(c.Block)));
|
||||
if (!applicableCodeChanges.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var change in applicableCodeChanges)
|
||||
{
|
||||
// If doing a code replacement, replace ReplaceSnippet in source with Block
|
||||
if (change.ReplaceSnippet != null)
|
||||
{
|
||||
var replaceSnippet = string.Join(System.Environment.NewLine, change.ReplaceSnippet);
|
||||
if (sourceFileString.Contains(replaceSnippet))
|
||||
{
|
||||
sourceFileString = sourceFileString.Replace(replaceSnippet, change.Block);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Generate readme file when replace snippets not found in file
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceFileString += change.Block; // Otherwise appending block to end of file
|
||||
}
|
||||
}
|
||||
|
||||
var updatedSourceText = SourceText.From(sourceFileString);
|
||||
return fileDoc.WithText(updatedSourceText);
|
||||
}
|
||||
|
||||
internal static async Task UpdateDocument(Document document)
|
||||
{
|
||||
var classFileTxt = await document.GetTextAsync();
|
||||
File.WriteAllText(document.Name, classFileTxt.ToString(), new UTF8Encoding(false));
|
||||
}
|
||||
|
||||
// Filter out CodeBlocks that are invalid using FilterOptions
|
||||
internal static CodeBlock[] FilterCodeBlocks(CodeBlock[] codeBlocks, CodeChangeOptions options)
|
||||
{
|
||||
var filteredCodeBlocks = new HashSet<CodeBlock>();
|
||||
if (codeBlocks != null && codeBlocks.Any() && options != null)
|
||||
{
|
||||
foreach (var codeBlock in codeBlocks)
|
||||
{
|
||||
if (FilterOptions(codeBlock.Options, options))
|
||||
{
|
||||
filteredCodeBlocks.Add(codeBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredCodeBlocks.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Take the tfm value in the csproj and use the Dictionary of variables to find its true value.
|
||||
/// </summary>
|
||||
/// <param name="tfm">value for <TargetFramework/> or '<TargetFrameworks/> in the csproj file</param>
|
||||
/// <param name="csprojVariables">dictionary with all csproj properties and values</param>
|
||||
/// <returns></returns>
|
||||
internal static string ProcessTfm(string tfm, Dictionary<string, string> csprojVariables)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tfm))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
bool tfmHasVars = true;
|
||||
while (tfmHasVars)
|
||||
{
|
||||
//if the value is in the tfm dictionary (valid values), return it.
|
||||
if (ProjectModelHelper.ShortTfmDictionary.Values.ToList().Contains(tfm, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return tfm;
|
||||
}
|
||||
//if the value has a variable (key) in it, replace it with its value.
|
||||
else if (tfm.Contains('$'))
|
||||
{
|
||||
foreach (var key in csprojVariables.Keys)
|
||||
{
|
||||
if (tfm.ContainsIgnoreCase(key) && csprojVariables.TryGetValue(key, out string? val))
|
||||
{
|
||||
tfm = tfm.Replace(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return tfm;
|
||||
}
|
||||
}
|
||||
return tfm;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
|
||||
public class AppSettings : IAppSettings
|
||||
{
|
||||
private readonly Dictionary<string, object> _settings;
|
||||
|
||||
public AppSettings()
|
||||
{
|
||||
_settings = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
GlobalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDictionary<string, string> GlobalProperties { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? GetSettings(string sectionName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sectionName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _settings.TryGetValue(sectionName, out var settings) ? settings : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddSettings(string sectionName, object settings)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sectionName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_settings[sectionName] = settings;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
|
||||
public static class AppSettingsExtensions
|
||||
{
|
||||
public static WorkspaceSettings Workspace(this IAppSettings settings)
|
||||
{
|
||||
return settings.GetSettings<WorkspaceSettings>("workspace") ?? new WorkspaceSettings();
|
||||
}
|
||||
|
||||
public static T? GetSettings<T>(this IAppSettings settings, string sectionName) where T : class
|
||||
{
|
||||
var sectionObject = settings.GetSettings(sectionName);
|
||||
|
||||
return sectionObject as T;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.MSBuild;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Extensions.Roslyn;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
/// <summary>
|
||||
/// Service that manages Roslyn workspace. It ensures that all projects of interest are loaded in the workspace and are up to date.
|
||||
/// - If user's input path is project we also always open it since we expect some contracts might
|
||||
/// want to get access to that project.
|
||||
/// - all other projects loaded to the workspace only per-request to avoid running DT builds on all
|
||||
/// full solution. When some caller need to ensure project is loaded (for example ProjectService
|
||||
/// when it creates a new instance of <see cref="IProject"/>) it should call OpenProjectAsync
|
||||
/// here and that would ensure project is loaded in the workspace.
|
||||
/// </summary>
|
||||
public class CodeService : ICodeService, IDisposable
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private MSBuildWorkspace? _workspace;
|
||||
private Compilation? _compilation;
|
||||
private readonly IAppSettings _settings;
|
||||
|
||||
public CodeService(IAppSettings settings, ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Workspace?> GetWorkspaceAsync()
|
||||
{
|
||||
return await GetMsBuildWorkspaceAsync(_settings.Workspace().InputPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryApplyChanges(Solution? solution)
|
||||
{
|
||||
if (solution is null || _workspace is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var success = _workspace?.TryApplyChanges(solution) == true;
|
||||
return success;
|
||||
}
|
||||
|
||||
private async Task<MSBuildWorkspace?> GetMsBuildWorkspaceAsync(string? path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return _workspace;
|
||||
}
|
||||
|
||||
if (_workspace is not null)
|
||||
{
|
||||
return _workspace;
|
||||
}
|
||||
|
||||
var workspace = MSBuildWorkspace.Create(_settings.GlobalProperties);
|
||||
workspace.WorkspaceFailed += OnWorkspaceFailed;
|
||||
workspace.LoadMetadataForReferencedProjects = true;
|
||||
await workspace.OpenProjectAsync(path).ConfigureAwait(false);
|
||||
_workspace = workspace;
|
||||
return _workspace;
|
||||
}
|
||||
|
||||
public async Task OpenProjectAsync(string projectPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(projectPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var workspace = await GetWorkspaceAsync();
|
||||
if (workspace is not MSBuildWorkspace msbuildWorkspace)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Project? project = msbuildWorkspace.CurrentSolution.GetProject(projectPath);
|
||||
if (project is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await msbuildWorkspace.OpenProjectAsync(projectPath).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogMessage(ex.ToString(), LogMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void UnloadWorkspace()
|
||||
{
|
||||
var workspace = _workspace;
|
||||
_workspace = null;
|
||||
_compilation = null;
|
||||
if (workspace is not null)
|
||||
{
|
||||
workspace.WorkspaceFailed -= OnWorkspaceFailed;
|
||||
workspace.CloseSolution();
|
||||
workspace.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ReloadWorkspaceAsync(string? projectPath)
|
||||
{
|
||||
UnloadWorkspace();
|
||||
|
||||
await GetMsBuildWorkspaceAsync(_settings.Workspace().InputPath);
|
||||
await OpenProjectAsync(projectPath!);
|
||||
}
|
||||
|
||||
private void OnWorkspaceFailed(object? sender, WorkspaceDiagnosticEventArgs e)
|
||||
{
|
||||
var diagnostic = e.Diagnostic!;
|
||||
_logger.LogMessage($"[{diagnostic.Kind}] Problem loading file in MSBuild workspace {diagnostic.Message}");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
UnloadWorkspace();
|
||||
}
|
||||
|
||||
public async Task<IList<ISymbol>> GetAllClassSymbolsAsync()
|
||||
{
|
||||
List<ISymbol> classSymbols = [];
|
||||
if (_compilation is null)
|
||||
{
|
||||
var workspace = await GetWorkspaceAsync();
|
||||
var project = workspace?.CurrentSolution?.GetProject(_settings.Workspace().InputPath);
|
||||
if (project is not null)
|
||||
{
|
||||
_compilation = await project.GetCompilationAsync();
|
||||
}
|
||||
}
|
||||
|
||||
var compilationClassSymbols = _compilation?.SyntaxTrees.SelectMany(tree =>
|
||||
{
|
||||
var model = _compilation.GetSemanticModel(tree);
|
||||
var allNodes = tree.GetRoot().DescendantNodes();
|
||||
return tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().Select(classSyntax =>
|
||||
{
|
||||
var classSymbol = model.GetDeclaredSymbol(classSyntax);
|
||||
return classSymbol;
|
||||
});
|
||||
}).Append(_compilation.GetEntryPoint(CancellationToken.None)?.ContainingType).ToList();
|
||||
|
||||
compilationClassSymbols?.ForEach(x =>
|
||||
{
|
||||
if (x is not null)
|
||||
{
|
||||
classSymbols.Add(x);
|
||||
}
|
||||
});
|
||||
|
||||
return classSymbols;
|
||||
}
|
||||
|
||||
public async Task<IList<Document>> GetAllDocumentsAsync()
|
||||
{
|
||||
var workspace = await GetWorkspaceAsync();
|
||||
var project = workspace?.CurrentSolution?.GetProject(_settings.Workspace().InputPath);
|
||||
if (project is not null)
|
||||
{
|
||||
return project.Documents.ToList();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public async Task<Document?> GetDocumentAsync(string? documentName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(documentName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var workspace = await GetWorkspaceAsync();
|
||||
var project = workspace?.CurrentSolution?.GetProject(_settings.Workspace().InputPath);
|
||||
if (project is not null)
|
||||
{
|
||||
return project.Documents.FirstOrDefault(x => x.Name.EndsWith(documentName));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.DotNet.Scaffolding.ComponentModel;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.General;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services
|
||||
{
|
||||
public class DotNetToolService : IDotNetToolService
|
||||
{
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
public DotNetToolService(IEnvironmentService environmentService, IFileSystem fileSystem)
|
||||
{
|
||||
_environmentService = environmentService;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
private IList<DotNetToolInfo>? _dotNetTools;
|
||||
public IList<DotNetToolInfo> GlobalDotNetTools
|
||||
{
|
||||
get
|
||||
{
|
||||
_dotNetTools ??= GetDotNetTools();
|
||||
return _dotNetTools;
|
||||
}
|
||||
}
|
||||
|
||||
public List<CommandInfo> GetCommands(string dotnetToolName)
|
||||
{
|
||||
List<CommandInfo>? commands = null;
|
||||
if (GlobalDotNetTools.FirstOrDefault(x => x.Command.Equals(dotnetToolName, StringComparison.OrdinalIgnoreCase)) != null)
|
||||
{
|
||||
var runner = DotnetCliRunner.Create(dotnetToolName, ["get-commands"]);
|
||||
var exitCode = runner.ExecuteAndCaptureOutput(out var stdOut, out var stdErr);
|
||||
if (exitCode == 0 && !string.IsNullOrEmpty(stdOut))
|
||||
{
|
||||
try
|
||||
{
|
||||
string escapedJsonString = stdOut.Replace("\r", "").Replace("\n", "");
|
||||
commands = JsonSerializer.Deserialize<List<CommandInfo>>(escapedJsonString);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commands ?? [];
|
||||
}
|
||||
|
||||
public DotNetToolInfo? GetDotNetTool(string? componentName, string? version = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(componentName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var matchingTools = GlobalDotNetTools.Where(x => x.Command.Equals(componentName, StringComparison.OrdinalIgnoreCase));
|
||||
if (string.IsNullOrEmpty(version))
|
||||
{
|
||||
return matchingTools.FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
return matchingTools.FirstOrDefault(x => x.Version.Equals(version));
|
||||
}
|
||||
}
|
||||
|
||||
public IList<KeyValuePair<string, CommandInfo>> GetAllCommandsParallel(IList<DotNetToolInfo>? components = null)
|
||||
{
|
||||
if (components is null || components.Count == 0)
|
||||
{
|
||||
components = GlobalDotNetTools;
|
||||
}
|
||||
|
||||
var options = new ParallelOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = System.Environment.ProcessorCount
|
||||
};
|
||||
|
||||
var commands = new ConcurrentBag<KeyValuePair<string, CommandInfo>>();
|
||||
Parallel.ForEach(components, options, dotnetTool =>
|
||||
{
|
||||
var commandInfo = GetCommands(dotnetTool.Command);
|
||||
if (commandInfo != null)
|
||||
{
|
||||
foreach (var cmd in commandInfo)
|
||||
{
|
||||
commands.Add(KeyValuePair.Create(dotnetTool.Command, cmd));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return commands.ToList();
|
||||
}
|
||||
|
||||
private static IList<DotNetToolInfo> GetDotNetTools()
|
||||
{
|
||||
var dotnetToolList = new List<DotNetToolInfo>();
|
||||
var runner = DotnetCliRunner.CreateDotNet("tool", ["list", "-g"]);
|
||||
var exitCode = runner.ExecuteAndCaptureOutput(out var stdOut, out var stdErr);
|
||||
if (exitCode == 0 && !string.IsNullOrEmpty(stdOut))
|
||||
{
|
||||
var stdOutByLine = stdOut.Split(System.Environment.NewLine);
|
||||
foreach (var line in stdOutByLine)
|
||||
{
|
||||
var parsedDotNetTool = ParseToolInfo(line);
|
||||
if (parsedDotNetTool != null &&
|
||||
!parsedDotNetTool.Command.Equals("dotnet-scaffold", StringComparison.OrdinalIgnoreCase) &&
|
||||
!parsedDotNetTool.PackageName.Equals("package", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
dotnetToolList.Add(parsedDotNetTool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dotnetToolList;
|
||||
}
|
||||
|
||||
private static DotNetToolInfo? ParseToolInfo(string line)
|
||||
{
|
||||
var match = Regex.Match(line, @"^(\S+)\s+(\S+)\s+(\S+)");
|
||||
if (match.Success)
|
||||
{
|
||||
return new DotNetToolInfo
|
||||
{
|
||||
PackageName = match.Groups[1].Value,
|
||||
Version = match.Groups[2].Value,
|
||||
Command = match.Groups[3].Value
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper over System.Environment abstraction for unit testing.
|
||||
/// </summary>
|
||||
public class EnvironmentService : IEnvironmentService
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
public EnvironmentService(IFileSystem fileSystem)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
private const string DOTNET_RUNNING_IN_CONTAINER = nameof(DOTNET_RUNNING_IN_CONTAINER);
|
||||
|
||||
private static string? _nugetCache;
|
||||
public static string LocalNugetCachePath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(_nugetCache))
|
||||
{
|
||||
var nugetPackagesEnvironmentVariable = System.Environment.GetEnvironmentVariable("NUGET_PACKAGES");
|
||||
|
||||
_nugetCache = string.IsNullOrWhiteSpace(nugetPackagesEnvironmentVariable)
|
||||
? Path.Combine(LocalUserProfilePath, ".nuget", "packages")
|
||||
: nugetPackagesEnvironmentVariable;
|
||||
}
|
||||
|
||||
return _nugetCache!;
|
||||
}
|
||||
}
|
||||
|
||||
public static string LocalUserProfilePath
|
||||
{
|
||||
get
|
||||
{
|
||||
return System.Environment.GetEnvironmentVariable(
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? "USERPROFILE"
|
||||
: "HOME") ?? "USERPROFILE";
|
||||
}
|
||||
}
|
||||
|
||||
public const string DotnetProfileDirectoryName = ".dotnet";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CurrentDirectory => System.Environment.CurrentDirectory;
|
||||
|
||||
/// <inheritdoc />
|
||||
public OperatingSystem OS => System.Environment.OSVersion;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Is64BitOperatingSystem => System.Environment.Is64BitOperatingSystem;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Is64BitProcess => System.Environment.Is64BitProcess;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UserProfilePath => LocalNugetCachePath;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? DomainName => System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string NugetCachePath => LocalNugetCachePath;
|
||||
|
||||
private string? _dotnetHomePath;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DotnetUserProfilePath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(_dotnetHomePath))
|
||||
{
|
||||
var homePath = System.Environment.GetEnvironmentVariable("DOTNET_CLI_HOME");
|
||||
if (string.IsNullOrEmpty(homePath))
|
||||
{
|
||||
homePath = UserProfilePath;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(homePath))
|
||||
{
|
||||
homePath = Path.Combine(LocalUserProfilePath, DotnetProfileDirectoryName);
|
||||
}
|
||||
|
||||
_dotnetHomePath = homePath ?? string.Empty;
|
||||
}
|
||||
|
||||
return _dotnetHomePath!;
|
||||
}
|
||||
}
|
||||
|
||||
private string? _localUserFolderPath;
|
||||
public string LocalUserFolderPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(_localUserFolderPath))
|
||||
{
|
||||
_localUserFolderPath = LocalUserProfilePath;
|
||||
}
|
||||
|
||||
return _localUserFolderPath!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetMachineName()
|
||||
{
|
||||
return System.Environment.MachineName;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? GetEnvironmentVariable(string name)
|
||||
{
|
||||
return System.Environment.GetEnvironmentVariable(name);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetEnvironmentVariable(string name, string value, EnvironmentVariableTarget envTarget)
|
||||
{
|
||||
System.Environment.SetEnvironmentVariable(name, value, envTarget);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetFolderPath(System.Environment.SpecialFolder specifalFolder)
|
||||
{
|
||||
return System.Environment.GetFolderPath(specifalFolder);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ExpandEnvironmentVariables(string name)
|
||||
{
|
||||
return System.Environment.ExpandEnvironmentVariables(name);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
using System.Composition;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
|
||||
|
||||
/// <summary>
|
||||
/// Sets environment variables for this process.
|
||||
/// </summary>
|
||||
public class EnvironmentVariablesStartup
|
||||
{
|
||||
private readonly IHostService _hostService;
|
||||
private readonly IEnvironmentService _environment;
|
||||
private readonly IAppSettings _settings;
|
||||
|
||||
public EnvironmentVariablesStartup(
|
||||
IHostService hostService,
|
||||
IEnvironmentService environment,
|
||||
IAppSettings settings)
|
||||
{
|
||||
_hostService = hostService;
|
||||
_environment = environment;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> StartupAsync()
|
||||
{
|
||||
var environmentVariables = await _hostService.GetEnvironmentVariablesAsync();
|
||||
|
||||
SetEnvironmentVariables(environmentVariables);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SetEnvironmentVariables(IDictionary<string, string> environmentVariables)
|
||||
{
|
||||
foreach (var kvp in environmentVariables)
|
||||
{
|
||||
if (string.IsNullOrEmpty(kvp.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
|
||||
_settings.GlobalProperties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
|
||||
|
||||
public class HostService : IHostService
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IEnumerable<IEnvironmentVariableProvider> _providers;
|
||||
private Dictionary<string, string>? _variables;
|
||||
|
||||
public HostService(ILogger logger, IEnumerable<IEnvironmentVariableProvider> providers)
|
||||
{
|
||||
_logger = logger;
|
||||
_providers = providers;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetInstallationPath()
|
||||
{
|
||||
return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IDictionary<string, string>> GetEnvironmentVariablesAsync()
|
||||
{
|
||||
if (_variables is null)
|
||||
{
|
||||
var variables = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var provider in _providers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var providerVariables = await provider.GetEnvironmentVariablesAsync();
|
||||
if (providerVariables is not null)
|
||||
{
|
||||
foreach (var kvp in providerVariables)
|
||||
{
|
||||
variables[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogMessage(ex.Message, LogMessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
_variables = variables;
|
||||
}
|
||||
|
||||
return _variables;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
|
||||
|
||||
public interface IEnvironmentService
|
||||
{
|
||||
string UserProfilePath { get; }
|
||||
string NugetCachePath { get; }
|
||||
string LocalUserFolderPath { get; }
|
||||
string DotnetUserProfilePath { get; }
|
||||
string CurrentDirectory { get; }
|
||||
bool Is64BitOperatingSystem { get; }
|
||||
bool Is64BitProcess { get; }
|
||||
string? DomainName { get; }
|
||||
OperatingSystem OS { get; }
|
||||
string GetMachineName();
|
||||
string? GetEnvironmentVariable(string name);
|
||||
void SetEnvironmentVariable(string name, string value, EnvironmentVariableTarget envTarget = EnvironmentVariableTarget.Process);
|
||||
string GetFolderPath(System.Environment.SpecialFolder specifalFolder);
|
||||
string ExpandEnvironmentVariables(string name);
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
|
||||
|
||||
public interface IEnvironmentVariableProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns environment variables to be set in current process.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
ValueTask<IEnumerable<KeyValuePair<string, string>>?> GetEnvironmentVariablesAsync();
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.General;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
|
||||
|
||||
public class MacMsbuildEnvironmentVariableProvider : IEnvironmentVariableProvider
|
||||
{
|
||||
private const string MacOSMonoFrameworkMSBuildExtensionsDir = "/Library/Frameworks/Mono.framework/External/xbuild";
|
||||
|
||||
private readonly IAppSettings _settings;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public MacMsbuildEnvironmentVariableProvider(IAppSettings settings, IFileSystem fileSystem, ILogger logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_fileSystem = fileSystem;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<IEnumerable<KeyValuePair<string, string>>?> GetEnvironmentVariablesAsync()
|
||||
{
|
||||
// Note: try to do it for Mac and Linux.
|
||||
return !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? GetMacEnvironmentVariablesAsync()
|
||||
: new ValueTask<IEnumerable<KeyValuePair<string, string>>?>((IEnumerable<KeyValuePair<string, string>>?)null);
|
||||
}
|
||||
|
||||
private ValueTask<IEnumerable<KeyValuePair<string, string>>?> GetMacEnvironmentVariablesAsync()
|
||||
{
|
||||
var variables = new Dictionary<string, string>();
|
||||
var msbuildPath = _settings.Workspace().MSBuildPath;
|
||||
|
||||
// If the user-settable MSBuildPath is null/empty at this point, then use the
|
||||
// "MSBuildExtensionsPath" environment variable which *should* have been set
|
||||
// by the MSBuildLocator.
|
||||
if (string.IsNullOrEmpty(msbuildPath))
|
||||
{
|
||||
msbuildPath = System.Environment.GetEnvironmentVariable("MSBuildExtensionsPath");
|
||||
}
|
||||
|
||||
var msbuildExtensionsPath = GetMacOSMSBuildExtensionsPath(msbuildPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(msbuildExtensionsPath))
|
||||
{
|
||||
_settings.Workspace().MSBuildPath = msbuildExtensionsPath;
|
||||
variables.Add(Constants.EnvironmentVariables.MSBuildExtensionsPath32, msbuildExtensionsPath);
|
||||
variables.Add(Constants.EnvironmentVariables.MSBuildExtensionsPath, msbuildExtensionsPath);
|
||||
|
||||
var msbuildExePath = Path.Combine(msbuildExtensionsPath, "MSBuild.dll");
|
||||
variables.Add(Constants.EnvironmentVariables.MSBUILD_EXE_PATH, msbuildExePath);
|
||||
|
||||
var msbuildSdksPath = Path.Combine(msbuildExtensionsPath, "Sdks");
|
||||
variables.Add(Constants.EnvironmentVariables.MSBuildSDKsPath, msbuildSdksPath);
|
||||
}
|
||||
|
||||
variables.Add(Constants.EnvironmentVariables.USERPROFILE, System.Environment.GetEnvironmentVariable("HOME") ?? string.Empty);
|
||||
|
||||
return new ValueTask<IEnumerable<KeyValuePair<string, string>>?>(variables);
|
||||
}
|
||||
|
||||
private string? GetMacOSMSBuildExtensionsPath(string? suppliedPath)
|
||||
{
|
||||
const string DefaultDotnetSdkLocation = "/usr/local/share/dotnet/sdk/";
|
||||
|
||||
if (string.IsNullOrEmpty(suppliedPath) || !suppliedPath.StartsWith(DefaultDotnetSdkLocation, StringComparison.Ordinal))
|
||||
{
|
||||
return suppliedPath;
|
||||
}
|
||||
|
||||
string? msbuildExtensionsPath = null;
|
||||
|
||||
if (_fileSystem.DirectoryExists(MacOSMonoFrameworkMSBuildExtensionsDir))
|
||||
{
|
||||
// Check to see if the specified MSBuildPath contains the Mono.framework build extensions.
|
||||
var monoExtensionDirectories = _fileSystem.EnumerateDirectories(MacOSMonoFrameworkMSBuildExtensionsDir, "*.*", SearchOption.TopDirectoryOnly);
|
||||
var createTempExtensionsDir = false;
|
||||
|
||||
foreach (var monoExtensionDir in monoExtensionDirectories)
|
||||
{
|
||||
var dotnetExtensionDir = Path.Combine(suppliedPath, Path.GetFileName(monoExtensionDir));
|
||||
if (!_fileSystem.DirectoryExists(dotnetExtensionDir))
|
||||
{
|
||||
createTempExtensionsDir = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the specified MSBuildPath does not contain the Mono.framework build extensions, create a temp
|
||||
// directory that we'll use to symlink everything.
|
||||
if (createTempExtensionsDir)
|
||||
{
|
||||
var homeDir = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile);
|
||||
//var versionDir = Path.GetFileName(suppliedPath.TrimEndPathSeparators());
|
||||
var versionDir = suppliedPath;
|
||||
msbuildExtensionsPath = Path.Combine(homeDir, ".dotnet-upgrade-assistant", "dotnet-sdk", versionDir);
|
||||
|
||||
if (!_fileSystem.DirectoryExists(msbuildExtensionsPath))
|
||||
{
|
||||
_fileSystem.CreateDirectory(msbuildExtensionsPath);
|
||||
}
|
||||
|
||||
// First, create symbolic links to all of the dotnet MSBuild file system entries.
|
||||
CreateSymbolicLinks(msbuildExtensionsPath, suppliedPath);
|
||||
|
||||
// Then create the symbolic links to the Mono.framework/External/xbuild system entries.
|
||||
CreateSymbolicLinks(msbuildExtensionsPath, MacOSMonoFrameworkMSBuildExtensionsDir);
|
||||
}
|
||||
}
|
||||
|
||||
return msbuildExtensionsPath ?? suppliedPath;
|
||||
}
|
||||
|
||||
private static void CreateSymbolicLinks(string targetDir, string sourceDir)
|
||||
{
|
||||
foreach (var entry in Directory.EnumerateFileSystemEntries(sourceDir))
|
||||
{
|
||||
var target = Path.Combine(targetDir, Path.GetFileName(entry));
|
||||
|
||||
var fileInfo = new FileInfo(target);
|
||||
if (fileInfo.Exists)
|
||||
{
|
||||
if (fileInfo.LinkTarget is not null && fileInfo.LinkTarget.Equals(entry, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
File.Delete(target);
|
||||
}
|
||||
else
|
||||
{
|
||||
var dirInfo = new DirectoryInfo(target);
|
||||
if (dirInfo.Exists)
|
||||
{
|
||||
if (dirInfo.LinkTarget is not null && dirInfo.LinkTarget.Equals(entry, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Directory.Delete(target);
|
||||
}
|
||||
}
|
||||
|
||||
File.CreateSymbolicLink(target, entry);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.VisualStudio.Setup.Configuration;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Environment;
|
||||
|
||||
#pragma warning disable CA5392 // Use DefaultDllImportSearchPaths attribute for P/Invokes
|
||||
#pragma warning disable SA1114 // Parameter list should follow declaration
|
||||
|
||||
internal static class NativeMethods
|
||||
{
|
||||
[DllImport("Microsoft.VisualStudio.Setup.Configuration.Native.dll", ExactSpelling = true, PreserveSig = true)]
|
||||
public static extern int GetSetupConfiguration(
|
||||
[MarshalAs(UnmanagedType.Interface)][Out] out ISetupConfiguration configuration,
|
||||
IntPtr reserved);
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.General;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Extensions;
|
||||
using Microsoft.VisualStudio.Setup.Configuration;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Environment;
|
||||
public class WindowsEnvironmentVariableProvider : IEnvironmentVariableProvider
|
||||
{
|
||||
private const int REGDB_E_CLASSNOTREG = unchecked((int)0x80040154);
|
||||
|
||||
private readonly IAppSettings _settings;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public WindowsEnvironmentVariableProvider(IAppSettings settings, IFileSystem fileSystem, ILogger logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_fileSystem = fileSystem;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<IEnumerable<KeyValuePair<string, string>>?> GetEnvironmentVariablesAsync()
|
||||
{
|
||||
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? GetWindowsEnvironmentVariablesAsync()
|
||||
: new ValueTask<IEnumerable<KeyValuePair<string, string>>?>((IEnumerable<KeyValuePair<string, string>>?)null);
|
||||
}
|
||||
|
||||
private ValueTask<IEnumerable<KeyValuePair<string, string>>?> GetWindowsEnvironmentVariablesAsync()
|
||||
{
|
||||
var variables = new Dictionary<string, string>();
|
||||
|
||||
var workspace = _settings.Workspace();
|
||||
var (visualStudioPath, visualStudioVersion) = GetLatestVisualStudioPath(workspace.VisualStudioPath);
|
||||
if (!string.IsNullOrEmpty(visualStudioPath))
|
||||
{
|
||||
// TODO mark this class as IStartup explicitly so it does initialize all settings it can.
|
||||
workspace.VisualStudioPath ??= visualStudioPath.WithOsPathSeparators();
|
||||
workspace.MSBuildPath ??= Path.Combine(visualStudioPath, "MSBuild");
|
||||
|
||||
variables.Add(Constants.EnvironmentVariables.VSINSTALLDIR, workspace.VisualStudioPath);
|
||||
variables.Add(Constants.EnvironmentVariables.MSBuildExtensionsPath32, workspace.MSBuildPath);
|
||||
variables.Add(Constants.EnvironmentVariables.MSBuildExtensionsPath, workspace.MSBuildPath);
|
||||
}
|
||||
|
||||
if (visualStudioVersion is int version)
|
||||
{
|
||||
workspace.VisualStudioVersion ??= $"{version}.0";
|
||||
|
||||
variables.Add("VisualStudioVersion", workspace.VisualStudioVersion);
|
||||
}
|
||||
|
||||
variables.Add(Constants.EnvironmentVariables.USERPROFILE, System.Environment.GetEnvironmentVariable(Constants.EnvironmentVariables.USERPROFILE) ?? string.Empty);
|
||||
|
||||
return new ValueTask<IEnumerable<KeyValuePair<string, string>>?>(variables);
|
||||
}
|
||||
|
||||
private (string? Path, int? Version) GetLatestVisualStudioPath(string? suppliedPath)
|
||||
{
|
||||
var latest = GetLatestVisualStudio(suppliedPath);
|
||||
|
||||
if (latest.InstallPath is null)
|
||||
{
|
||||
_logger.LogMessage("Did not find a Visual Studio instance", LogMessageType.Information);
|
||||
return default;
|
||||
}
|
||||
|
||||
if (_fileSystem.DirectoryExists(latest.InstallPath))
|
||||
{
|
||||
return (latest.InstallPath, latest.Version.Major);
|
||||
}
|
||||
else
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
private (string? InstallPath, Version Version) GetLatestVisualStudio(string? suppliedPath)
|
||||
{
|
||||
var resultVersion = new Version(0, 0);
|
||||
string? resultPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
// This code is not obvious. See the sample (link above) for reference.
|
||||
var query = (ISetupConfiguration2)GetQuery();
|
||||
var e = query.EnumAllInstances();
|
||||
|
||||
int fetched;
|
||||
var instances = new ISetupInstance[1];
|
||||
do
|
||||
{
|
||||
// Call e.Next to query for the next instance (single item or nothing returned).
|
||||
e.Next(1, instances, out fetched);
|
||||
if (fetched <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var instance = (ISetupInstance2)instances[0];
|
||||
var state = instance.GetState();
|
||||
|
||||
if (!Version.TryParse(instance.GetInstallationVersion(), out var version))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the install was complete and a valid version, consider it.
|
||||
if (state == InstanceState.Complete ||
|
||||
(state.HasFlag(InstanceState.Registered) && state.HasFlag(InstanceState.NoRebootRequired)))
|
||||
{
|
||||
var instanceHasMSBuild = false;
|
||||
|
||||
foreach (var package in instance.GetPackages())
|
||||
{
|
||||
if (string.Equals(package.GetId(), "Microsoft.Component.MSBuild", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
instanceHasMSBuild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (instanceHasMSBuild && instance is not null)
|
||||
{
|
||||
var installPath = instance.GetInstallationPath();
|
||||
|
||||
if (suppliedPath is not null && string.Equals(suppliedPath, installPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (installPath, version);
|
||||
}
|
||||
else if (version > resultVersion)
|
||||
{
|
||||
resultPath = installPath;
|
||||
resultVersion = version;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while (fetched > 0);
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
// This is OK, VS "15" or greater likely not installed.
|
||||
}
|
||||
|
||||
return (resultPath, resultVersion);
|
||||
}
|
||||
|
||||
private static ISetupConfiguration GetQuery()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to CoCreate the class object.
|
||||
return new SetupConfiguration();
|
||||
}
|
||||
catch (COMException ex) when (ex.ErrorCode == REGDB_E_CLASSNOTREG)
|
||||
{
|
||||
// Try to get the class object using app-local call.
|
||||
var result = NativeMethods.GetSetupConfiguration(out var query, IntPtr.Zero);
|
||||
|
||||
if (result < 0)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// The default implementation of <see cref="IFileSystem"/>
|
||||
/// used by product code. This just makes calls to methods in System.IO
|
||||
/// </summary>
|
||||
public class FileSystem : IFileSystem
|
||||
{
|
||||
private static IFileSystem? _fileSystem;
|
||||
public static IFileSystem Instance => _fileSystem ??= new FileSystem();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool FileExists(string path)
|
||||
{
|
||||
return File.Exists(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool DirectoryExists(string dirPath)
|
||||
{
|
||||
return Directory.Exists(dirPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateDirectory(string dirPath)
|
||||
{
|
||||
Directory.CreateDirectory(dirPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ReadAllText(string filePath)
|
||||
{
|
||||
return File.ReadAllText(filePath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteAllText(string filePath, string content)
|
||||
{
|
||||
File.WriteAllText(filePath, content);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string[] ReadAllLines(string filePath)
|
||||
{
|
||||
return File.ReadAllLines(filePath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteAllLines(string filePath, string[] content)
|
||||
{
|
||||
File.WriteAllLines(filePath, content);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Stream OpenFileStream(string path, FileMode mode, FileAccess access, FileShare share)
|
||||
{
|
||||
return new FileStream(path, mode, access, share);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> EnumerateDirectories(string path, string searchPattern, SearchOption searchOption)
|
||||
{
|
||||
return Directory.EnumerateDirectories(path, searchPattern, searchOption);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOption)
|
||||
{
|
||||
return Directory.EnumerateFiles(path, searchPattern, searchOption);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteFile(string filePath)
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CopyFile(string sourcePath, string destinationPath, bool overwrite)
|
||||
{
|
||||
File.Copy(sourcePath, destinationPath, overwrite);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetTempPath()
|
||||
{
|
||||
return Path.GetTempPath();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime GetLastWriteTime(string filePath)
|
||||
{
|
||||
return File.GetLastWriteTime(filePath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? GetFileVersion(string filePath)
|
||||
{
|
||||
return FileVersionInfo.GetVersionInfo(filePath)?.FileVersion;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version? GetAssemblyVersion(string filePath)
|
||||
{
|
||||
return AssemblyName.GetAssemblyName(filePath)?.Version;
|
||||
}
|
||||
|
||||
public void MakeFileWritable(string path)
|
||||
{
|
||||
Debug.Assert(File.Exists(path));
|
||||
|
||||
FileAttributes attributes = File.GetAttributes(path);
|
||||
if (attributes.HasFlag(FileAttributes.ReadOnly))
|
||||
{
|
||||
File.SetAttributes(path, attributes & ~FileAttributes.ReadOnly);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddFileAsync(string outputPath, Stream sourceStream)
|
||||
{
|
||||
using (var writeStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
await sourceStream.CopyToAsync(writeStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
|
||||
public interface IAppSettings
|
||||
{
|
||||
IDictionary<string, string> GlobalProperties { get; }
|
||||
object? GetSettings(string sectionName);
|
||||
void AddSettings(string sectionName, object settings);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
|
||||
public interface ICodeService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns current Roslyn workspace object.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<Workspace?> GetWorkspaceAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to apply changes to current Roslyn workspace and returns true if it was successful.
|
||||
/// </summary>
|
||||
/// <param name="solution"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
bool TryApplyChanges(Solution? solution);
|
||||
|
||||
/// <summary>
|
||||
/// Reloads Roslyn workspace.
|
||||
/// </summary>
|
||||
/// <param name="projectPath"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task ReloadWorkspaceAsync(string? projectPath);
|
||||
|
||||
Task <IList<ISymbol>> GetAllClassSymbolsAsync();
|
||||
Task<IList<Document>> GetAllDocumentsAsync();
|
||||
Task<Document?> GetDocumentAsync(string? documentName);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using Microsoft.DotNet.Scaffolding.ComponentModel;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
|
||||
public interface IDotNetToolService
|
||||
{
|
||||
IList<DotNetToolInfo> GlobalDotNetTools { get; }
|
||||
IList<KeyValuePair<string, CommandInfo>> GetAllCommandsParallel(IList<DotNetToolInfo>? components = null);
|
||||
DotNetToolInfo? GetDotNetTool(string? componentName, string? version = null);
|
||||
List<CommandInfo> GetCommands(string dotnetToolName);
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
|
||||
///<summary>
|
||||
/// A wrapper interface to be used for all file system related operations for easy unit testing.
|
||||
/// Any component that does some file system operations should only talk to this interface but not directly
|
||||
/// to System.IO implementations. Unit tests can then provide a mock implementation of
|
||||
/// this interface for testing that component.
|
||||
///</summary>
|
||||
public interface IFileSystem
|
||||
{
|
||||
bool FileExists(string path);
|
||||
|
||||
bool DirectoryExists(string dirPath);
|
||||
|
||||
void CreateDirectory(string dirPath);
|
||||
|
||||
string ReadAllText(string filePath);
|
||||
|
||||
string[] ReadAllLines(string filePath);
|
||||
|
||||
void WriteAllText(string filePath, string content);
|
||||
|
||||
void WriteAllLines(string filePath, string[] content);
|
||||
|
||||
Stream OpenFileStream(string path, FileMode mode, FileAccess access, FileShare share);
|
||||
|
||||
IEnumerable<string> EnumerateDirectories(string path, string searchPattern, SearchOption searchOption);
|
||||
|
||||
IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOption);
|
||||
|
||||
void DeleteFile(string filePath);
|
||||
|
||||
void CopyFile(string sourcePath, string destinationPath, bool overwrite);
|
||||
|
||||
string GetTempPath();
|
||||
|
||||
DateTime GetLastWriteTime(string filePath);
|
||||
|
||||
string? GetFileVersion(string filePath);
|
||||
|
||||
Version? GetAssemblyVersion(string filePath);
|
||||
|
||||
void MakeFileWritable(string filePath);
|
||||
|
||||
Task AddFileAsync(string filePath, Stream fileStream);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Abstracts out host properties. Host represents a tool running a the moment: VS, CLI etc.
|
||||
/// </summary>
|
||||
public interface IHostService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns host installation directory.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string GetInstallationPath();
|
||||
|
||||
/// <summary>
|
||||
/// Returns host specific environment variables to be set in current process.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
ValueTask<IDictionary<string, string>> GetEnvironmentVariablesAsync();
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using Microsoft.Build.Evaluation;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services
|
||||
{
|
||||
public interface IProjectService
|
||||
{
|
||||
Project Project { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services
|
||||
{
|
||||
public class ConsoleLogger : ILogger
|
||||
{
|
||||
private readonly bool _jsonOutput;
|
||||
private readonly bool _silent;
|
||||
private static bool isTrace = !string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("codegen_trace"));
|
||||
public bool IsTracing => isTrace;
|
||||
|
||||
private string CommandName { get; }
|
||||
|
||||
public ConsoleLogger(string? commandName = null, bool jsonOutput = false, bool silent = false)
|
||||
{
|
||||
CommandName = commandName ?? string.Empty;
|
||||
_jsonOutput = jsonOutput;
|
||||
_silent = silent;
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
}
|
||||
|
||||
public void LogMessage(string? message, LogMessageType level, bool removeNewLine = false)
|
||||
{
|
||||
//if json output is enabled, don't write to console at all.
|
||||
if (!_silent && !_jsonOutput)
|
||||
{
|
||||
switch (level)
|
||||
{
|
||||
case LogMessageType.Error:
|
||||
if (removeNewLine)
|
||||
{
|
||||
Console.Error.Write(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine(message);
|
||||
}
|
||||
break;
|
||||
case LogMessageType.Information:
|
||||
if (removeNewLine)
|
||||
{
|
||||
Console.Write(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LogJsonMessage(string? state = null, object? content = null, string? output = null)
|
||||
{
|
||||
if (!_silent)
|
||||
{
|
||||
if (_jsonOutput)
|
||||
{
|
||||
var jsonMessage = new JsonResponse(CommandName, state, content, output);
|
||||
Console.WriteLine(jsonMessage.ToJsonString());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (state == State.Fail)
|
||||
{
|
||||
LogMessage(output, LogMessageType.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMessage(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LogMessage(string? message, bool removeNewLine = false)
|
||||
{
|
||||
if (!_silent && !_jsonOutput)
|
||||
{
|
||||
LogMessage(message, LogMessageType.Information, removeNewLine);
|
||||
}
|
||||
}
|
||||
|
||||
public void LogFailureAndExit(string failureMessage)
|
||||
{
|
||||
if (_jsonOutput)
|
||||
{
|
||||
LogJsonMessage(State.Fail, output: failureMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMessage(failureMessage, LogMessageType.Error);
|
||||
}
|
||||
|
||||
System.Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void LogMessage(string message, LogMessageType level, bool removeNewLine = false);
|
||||
void LogMessage(string message, bool removeNewLine = false);
|
||||
void LogJsonMessage(string? state = null, object? content = null, string? output = null);
|
||||
void LogFailureAndExit(string failureMessage);
|
||||
}
|
||||
|
||||
public enum LogMessageType
|
||||
{
|
||||
Error,
|
||||
Information,
|
||||
Trace
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services
|
||||
{
|
||||
public class JsonResponse
|
||||
{
|
||||
public string Command { get; }
|
||||
public string? State { get; set; }
|
||||
public object? Content { get; set; }
|
||||
public string? Output { get; set; }
|
||||
|
||||
public JsonResponse(string command, string? state = null, object? content = null, string? output = null)
|
||||
{
|
||||
Command = command;
|
||||
State = state;
|
||||
Content = content;
|
||||
Output = output;
|
||||
}
|
||||
|
||||
public string ToJsonString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
}
|
||||
}
|
||||
|
||||
public class State
|
||||
{
|
||||
public const string Success = nameof(Success);
|
||||
public const string Processing = nameof(Processing);
|
||||
public const string Fail = nameof(Fail);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using Microsoft.Build.Locator;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
|
||||
public class MsBuildInitializer
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public MsBuildInitializer(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
RegisterMsbuild();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// use MSBuildLocator to find MSBuild instances, return the first (highest version found).
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private string RegisterMsbuild()
|
||||
{
|
||||
var msbuildPath = FindMSBuildPath();
|
||||
if (string.IsNullOrEmpty(msbuildPath))
|
||||
{
|
||||
_logger.LogMessage($"No msbuild path found!", LogMessageType.Error);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var instance = MSBuildLocator.QueryVisualStudioInstances()
|
||||
.FirstOrDefault(i => string.Equals(msbuildPath, i.MSBuildPath, StringComparison.OrdinalIgnoreCase));
|
||||
if (instance is null)
|
||||
{
|
||||
_logger.LogMessage($"No msbuild find out at path '{msbuildPath}'!", LogMessageType.Error);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (!MSBuildLocator.IsRegistered)
|
||||
{
|
||||
// Must register instance rather than just path so everything gets set correctly for .NET SDK instances
|
||||
MSBuildLocator.RegisterInstance(instance);
|
||||
|
||||
}
|
||||
|
||||
var resolver = new AssemblyDependencyResolver(msbuildPath);
|
||||
|
||||
AssemblyLoadContext.Default.Resolving += ResolveAssembly;
|
||||
|
||||
var version = instance.Version.ToString();
|
||||
return version;
|
||||
|
||||
Assembly? ResolveAssembly(AssemblyLoadContext context, AssemblyName assemblyName)
|
||||
{
|
||||
if (context is null || assemblyName is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (resolver.ResolveAssemblyToPath(assemblyName) is string path)
|
||||
{
|
||||
return context.LoadFromAssemblyPath(path);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string? FindMSBuildPath()
|
||||
{
|
||||
var msBuildInstances = FilterForBitness(MSBuildLocator.QueryVisualStudioInstances())
|
||||
.OrderByDescending(m => m.Version)
|
||||
.ToList();
|
||||
|
||||
if (msBuildInstances.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
return msBuildInstances.FirstOrDefault()?.MSBuildPath;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<VisualStudioInstance> FilterForBitness(IEnumerable<VisualStudioInstance> instances)
|
||||
{
|
||||
foreach (var instance in instances)
|
||||
{
|
||||
var is32bit = instance.MSBuildPath.Contains("x86", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (System.Environment.Is64BitProcess == !is32bit)
|
||||
{
|
||||
yield return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
using Microsoft.Build.Evaluation;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
public class ProjectService : IProjectService, IDisposable
|
||||
{
|
||||
private readonly string _projectPath;
|
||||
private readonly ILogger _logger;
|
||||
private readonly bool _shouldUnload;
|
||||
|
||||
public ProjectService(string projectPath, ILogger logger)
|
||||
{
|
||||
_projectPath = projectPath ?? string.Empty;
|
||||
_logger = logger;
|
||||
var projects = ProjectCollection.GlobalProjectCollection.GetLoadedProjects(_projectPath);
|
||||
_shouldUnload = projects.Count == 0;
|
||||
Project = projects.SingleOrDefault() ?? ProjectCollection.GlobalProjectCollection.LoadProject(_projectPath);
|
||||
Project.Build();
|
||||
}
|
||||
|
||||
public Project Project { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_shouldUnload && Project is not null)
|
||||
{
|
||||
// Unload it again if we were the ones who loaded it.
|
||||
// Unload both, project and project root element, to remove it from strong and weak MSBuild caches.
|
||||
ProjectCollection.GlobalProjectCollection.UnloadProject(Project);
|
||||
ProjectCollection.GlobalProjectCollection.UnloadProject(Project.Xml);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Extensions;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Workspace settings are initialized in following order:
|
||||
///
|
||||
/// - by default we get values from the environment variables
|
||||
/// - then when command line arguments are passed these settings could be overridden
|
||||
/// - then when we discover any environment variables to be set/modified during IStartup executions,
|
||||
/// we could set those settings if they were not provided before by earlier steps.
|
||||
///
|
||||
/// This way we let shell environment variables override what we could potentially pick for msbuild or VS paths,
|
||||
/// then user could explicitly specify those paths, and only then if nothing happened we would set them ourselves
|
||||
/// to the value we think is the best on current machine.
|
||||
/// </summary>
|
||||
public class WorkspaceSettings
|
||||
{
|
||||
public WorkspaceSettings()
|
||||
{
|
||||
MSBuildPath = System.Environment.GetEnvironmentVariable("MSBuildExtensionsPath");
|
||||
VisualStudioPath = System.Environment.GetEnvironmentVariable("VSINSTALLDIR");
|
||||
VisualStudioVersion = System.Environment.GetEnvironmentVariable("VisualStudioVersion");
|
||||
}
|
||||
|
||||
private string? _inputPath;
|
||||
public string? InputPath
|
||||
{
|
||||
get => _inputPath;
|
||||
set => _inputPath = value?.WithOsPathSeparators();
|
||||
}
|
||||
|
||||
private string? _msbuildPath;
|
||||
public string? MSBuildPath
|
||||
{
|
||||
get => _msbuildPath;
|
||||
set => _msbuildPath = value?.WithOsPathSeparators();
|
||||
}
|
||||
|
||||
private string? _visualStudioPath;
|
||||
public string? VisualStudioPath
|
||||
{
|
||||
get => _visualStudioPath;
|
||||
set => _visualStudioPath = value?.WithOsPathSeparators();
|
||||
}
|
||||
|
||||
public string? VisualStudioVersion { get; set; }
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.T4Templating;
|
||||
|
||||
/// <summary>
|
||||
/// switch out the CallContext.LogicalGetData call in the generated .cs template with this
|
||||
/// </summary>
|
||||
public sealed class CallContext
|
||||
{
|
||||
public static object? LogicalGetData(string name)
|
||||
=> null;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
namespace Microsoft.DotNet.Scaffolding.Helpers.T4Templating;
|
||||
|
||||
/// <summary>
|
||||
/// Services for invoking T4 text templates for code generation.
|
||||
/// </summary>
|
||||
public interface ITemplateInvoker
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes a T4 text template and returns the result.
|
||||
/// </summary>
|
||||
/// <param name="template">ITextTransformation template object.</param>
|
||||
/// <param name="templateParameters">Parameters for template execution.
|
||||
/// These parameters can be accessed in text template using a parameter directive.
|
||||
/// The values passed in must be either serializable or
|
||||
/// extend <see cref="System.MarshalByRefObject"/> type.</param>
|
||||
/// <returns>Generated code if there were no processing errors. Throws
|
||||
/// <see cref="System.InvalidOperationException" /> otherwise.
|
||||
/// </returns>
|
||||
string InvokeTemplate(
|
||||
ITextTransformation template,
|
||||
IDictionary<string, object> templateParameters);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using System.CodeDom.Compiler;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.T4Templating;
|
||||
|
||||
/// <summary>
|
||||
/// This is currently an internal API that supports scaffolding. Use with caution.
|
||||
/// </summary>
|
||||
public interface ITextTransformation
|
||||
{
|
||||
/// <summary>
|
||||
/// Session object that can be used to transmit information into a template.
|
||||
/// </summary>
|
||||
IDictionary<string, object> Session { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Errors received after performing the text transformation.
|
||||
/// </summary>
|
||||
CompilerErrorCollection Errors { get; }
|
||||
|
||||
void Initialize();
|
||||
|
||||
string TransformText();
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
using System.CodeDom.Compiler;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.Helpers.T4Templating;
|
||||
|
||||
/// <summary>
|
||||
/// Contains useful helper functions for running visual studio text transformation.
|
||||
/// For internal microsoft use only. Use <see cref="ITemplateInvoker"/>
|
||||
/// in custom code generators.
|
||||
/// </summary>
|
||||
public class TemplateInvoker : ITemplateInvoker
|
||||
{
|
||||
private readonly ConsoleLogger _consoleLogger;
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public TemplateInvoker(ConsoleLogger? consoleLogger = null)
|
||||
{
|
||||
_consoleLogger = consoleLogger ?? new ConsoleLogger();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a code generator template to generate the code.
|
||||
/// </summary>
|
||||
/// <param name="template">ITextTransformation template object</param>
|
||||
/// <param name="templateParameters">Parameters for the template.
|
||||
/// These parameters can be accessed in text template using a parameter directive.
|
||||
/// The values passed in must be either serializable or
|
||||
/// extend <see cref="MarshalByRefObject"/> type.</param>
|
||||
/// <returns>Generated code if there were no processing errors. Throws
|
||||
/// <see cref="InvalidOperationException" /> otherwise.
|
||||
/// </returns>
|
||||
public string InvokeTemplate(ITextTransformation template, IDictionary<string, object> templateParameters)
|
||||
{
|
||||
if (template is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (templateParameters is not null && templateParameters.Any())
|
||||
{
|
||||
foreach (var param in templateParameters)
|
||||
{
|
||||
template.Session.Add(param.Key, param.Value);
|
||||
}
|
||||
}
|
||||
|
||||
template.Initialize();
|
||||
return ProcessTemplate(template);
|
||||
}
|
||||
|
||||
private string ProcessTemplate(ITextTransformation transformation)
|
||||
{
|
||||
var output = transformation.TransformText();
|
||||
if (transformation.Errors.HasErrors)
|
||||
{
|
||||
foreach (CompilerError error in transformation.Errors)
|
||||
{
|
||||
_consoleLogger.LogMessage(error.ErrorText, LogMessageType.Error);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Processing '{transformation.GetType().Name}' failed");
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.CodeDom.Compiler;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyModel;
|
||||
using Microsoft.VisualStudio.TextTemplating;
|
||||
|
||||
namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// This is an internal API that supports the T4 Templating infrastructure and not subject to
|
||||
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
|
||||
/// any release. You should only use it directly in your code with extreme caution and knowing that
|
||||
/// doing so can result in application failures when updating to a new scaffolding release.
|
||||
/// Referencing from dotnet/efcore/src/EFCore.Design/Scaffolding/Internal/TextTemplatingEngineHost.cs
|
||||
/// </summary>
|
||||
public class TextTemplatingEngineHost : ITextTemplatingSessionHost, ITextTemplatingEngineHost, IServiceProvider
|
||||
{
|
||||
private static readonly List<string> _noWarn = ["CS1701", "CS1702"];
|
||||
|
||||
private readonly IServiceProvider? _serviceProvider;
|
||||
private ITextTemplatingSession? _session;
|
||||
private CompilerErrorCollection? _errors;
|
||||
private string? _extension;
|
||||
private Encoding? _outputEncoding;
|
||||
private bool _fromOutputDirective;
|
||||
|
||||
public TextTemplatingEngineHost(IServiceProvider? serviceProvider = null)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
[AllowNull]
|
||||
public virtual ITextTemplatingSession Session
|
||||
{
|
||||
get => _session ??= CreateSession();
|
||||
set => _session = value;
|
||||
}
|
||||
|
||||
public virtual IList<string> StandardAssemblyReferences { get; } = new[]
|
||||
{
|
||||
typeof(ITextTemplatingEngineHost).Assembly.Location, typeof(CompilerErrorCollection).Assembly.Location
|
||||
};
|
||||
|
||||
public virtual IList<string> StandardImports { get; } = new[] { "System" };
|
||||
|
||||
public virtual string? TemplateFile { get; set; }
|
||||
|
||||
public virtual string Extension
|
||||
=> _extension ?? ".cs";
|
||||
|
||||
public virtual CompilerErrorCollection Errors
|
||||
=> _errors ??= [];
|
||||
|
||||
public virtual Encoding OutputEncoding
|
||||
=> _outputEncoding ?? Encoding.UTF8;
|
||||
|
||||
public virtual void Initialize()
|
||||
{
|
||||
_session?.Clear();
|
||||
_errors = null;
|
||||
_extension = null;
|
||||
_outputEncoding = null;
|
||||
_fromOutputDirective = false;
|
||||
}
|
||||
|
||||
public virtual ITextTemplatingSession CreateSession()
|
||||
=> new TextTemplatingSession();
|
||||
|
||||
public virtual object? GetHostOption(string optionName)
|
||||
=> null;
|
||||
|
||||
public virtual bool LoadIncludeText(string requestFileName, out string content, out string location)
|
||||
{
|
||||
location = ResolvePath(requestFileName);
|
||||
var exists = File.Exists(location);
|
||||
content = exists
|
||||
? File.ReadAllText(location)
|
||||
: string.Empty;
|
||||
|
||||
return exists;
|
||||
}
|
||||
|
||||
public virtual void LogErrors(CompilerErrorCollection errors)
|
||||
=> Errors.AddRange(errors.Cast<CompilerError>().Where(e => !_noWarn.Contains(e.ErrorNumber)).ToArray());
|
||||
|
||||
public virtual AppDomain ProvideTemplatingAppDomain(string content)
|
||||
=> AppDomain.CurrentDomain;
|
||||
|
||||
public virtual string ResolveAssemblyReference(string assemblyReference)
|
||||
{
|
||||
if (Path.IsPathRooted(assemblyReference))
|
||||
{
|
||||
return assemblyReference;
|
||||
}
|
||||
|
||||
var path = DependencyContext.Default?.CompileLibraries
|
||||
.FirstOrDefault(l => l.Assemblies.Any(a => Path.GetFileNameWithoutExtension(a) == assemblyReference))
|
||||
?.ResolveReferencePaths()
|
||||
.First(p => Path.GetFileNameWithoutExtension(p) == assemblyReference);
|
||||
if (path is not null)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Assembly.Load(assemblyReference).Location;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// TODO: Expand variables?
|
||||
return assemblyReference;
|
||||
}
|
||||
|
||||
public virtual Type ResolveDirectiveProcessor(string processorName)
|
||||
=> throw new FileNotFoundException($"Failed to resolve type for directive processor {processorName}");
|
||||
|
||||
public virtual string ResolveParameterValue(string directiveId, string processorName, string parameterName)
|
||||
=> string.Empty;
|
||||
|
||||
public virtual string ResolvePath(string path)
|
||||
=> !Path.IsPathRooted(path) && Path.IsPathRooted(TemplateFile)
|
||||
? Path.Combine(Path.GetDirectoryName(TemplateFile)!, path)
|
||||
: path;
|
||||
|
||||
public virtual void SetFileExtension(string extension)
|
||||
=> _extension = extension;
|
||||
|
||||
public virtual void SetOutputEncoding(Encoding encoding, bool fromOutputDirective)
|
||||
{
|
||||
if (_fromOutputDirective)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_outputEncoding = encoding;
|
||||
_fromOutputDirective = fromOutputDirective;
|
||||
}
|
||||
|
||||
public virtual object? GetService(Type serviceType)
|
||||
=> _serviceProvider?.GetService(serviceType);
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.DotNet.Scaffolding.ComponentModel.Tests
|
||||
{
|
||||
public class CommandInfoTests
|
||||
{
|
||||
private static List<CommandInfo> CommandInfos;
|
||||
private static Parameter ValidParameter;
|
||||
private static Parameter PartialParameter;
|
||||
private static Parameter PartialParameter2;
|
||||
private static CommandInfo ValidCommandInfo;
|
||||
private static CommandInfo PartialCommandInfo;
|
||||
private static CommandInfo PartialCommandInfo2;
|
||||
|
||||
public CommandInfoTests()
|
||||
{
|
||||
ValidParameter = new()
|
||||
{
|
||||
Name = "parameter",
|
||||
DisplayName = "Param Display Name",
|
||||
Required = true,
|
||||
Description = "Parameter description",
|
||||
Type = BaseTypes.String,
|
||||
PickerType = InteractivePickerType.DbProviderPicker
|
||||
};
|
||||
|
||||
PartialParameter = new()
|
||||
{
|
||||
Name = string.Empty,
|
||||
DisplayName = "Param Display Name 2",
|
||||
Required = true,
|
||||
Type = BaseTypes.Int
|
||||
};
|
||||
|
||||
PartialParameter2 = new()
|
||||
{
|
||||
Name = "parameter2",
|
||||
DisplayName = "Param Display Name 3",
|
||||
Required = true,
|
||||
Type = BaseTypes.Int,
|
||||
Description = null,
|
||||
PickerType = null
|
||||
};
|
||||
|
||||
ValidCommandInfo = new()
|
||||
{
|
||||
Name = "command1",
|
||||
DisplayName = "Command 1",
|
||||
Description = "Description 1",
|
||||
Parameters = [ValidParameter]
|
||||
};
|
||||
|
||||
PartialCommandInfo = new()
|
||||
{
|
||||
Name = "command2",
|
||||
DisplayName = "Command 2",
|
||||
Parameters =
|
||||
[
|
||||
ValidParameter, PartialParameter
|
||||
]
|
||||
};
|
||||
|
||||
PartialCommandInfo2 = new()
|
||||
{
|
||||
Name = "command3",
|
||||
DisplayName = "Command 3",
|
||||
Description = null,
|
||||
Parameters =
|
||||
[
|
||||
ValidParameter, PartialParameter, PartialParameter2
|
||||
]
|
||||
};
|
||||
|
||||
CommandInfos =
|
||||
[
|
||||
ValidCommandInfo, PartialCommandInfo, PartialCommandInfo2
|
||||
];
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerialzationAndDeserializationTests()
|
||||
{
|
||||
var serializedJson = System.Text.Json.JsonSerializer.Serialize(CommandInfos);
|
||||
Assert.False(string.IsNullOrEmpty(serializedJson));
|
||||
Assert.True(serializedJson.Contains("\"Name\":\"command1\"", System.StringComparison.OrdinalIgnoreCase));
|
||||
Assert.True(serializedJson.Contains("\"Name\":\"command2\"", System.StringComparison.OrdinalIgnoreCase));
|
||||
Assert.True(serializedJson.Contains("\"Name\":\"command3\"", System.StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var deserializedJson = System.Text.Json.JsonSerializer.Deserialize<List<CommandInfo>>(serializedJson);
|
||||
Assert.NotNull(deserializedJson);
|
||||
Assert.True(deserializedJson.Count == 3);
|
||||
|
||||
var validCommandInfoDeserialized = deserializedJson[0];
|
||||
var partialCommandInfoDeserialized = deserializedJson[1];
|
||||
var partialCommandInfo2Deserialized = deserializedJson[2];
|
||||
Assert.NotNull(validCommandInfoDeserialized);
|
||||
Assert.NotNull(partialCommandInfoDeserialized);
|
||||
Assert.NotNull(partialCommandInfo2Deserialized);
|
||||
|
||||
Assert.Contains(validCommandInfoDeserialized.Parameters, x => x.Name.Equals("parameter"));
|
||||
Assert.Contains(partialCommandInfoDeserialized.Parameters, x => x.Name.Equals(PartialParameter.Name));
|
||||
Assert.Contains(partialCommandInfo2Deserialized.Parameters, x => x.Description is null);
|
||||
Assert.Contains(partialCommandInfo2Deserialized.Parameters, x => x.PickerType is null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(StandardTestTfms)</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(RepoRoot)\src\Shared\Microsoft.DotNet.Scaffolding.ComponentModel\Microsoft.DotNet.Scaffolding.ComponentModel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold;
|
||||
|
||||
public class ScaffoldCommandApp
|
||||
{
|
||||
private readonly string[] _args;
|
||||
private readonly Spectre.Console.Cli.CommandApp<ScaffoldCommand> _commandApp;
|
||||
|
||||
public ScaffoldCommandApp(Spectre.Console.Cli.CommandApp<ScaffoldCommand> commandApp, string[] args)
|
||||
{
|
||||
_commandApp = commandApp;
|
||||
_args = args;
|
||||
}
|
||||
|
||||
public Task<int> RunAsync()
|
||||
{
|
||||
return _commandApp.RunAsync(_args);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Reflection;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Environment;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
|
||||
using Microsoft.DotNet.Tools.Scaffold.AppBuilder;
|
||||
using Microsoft.DotNet.Tools.Scaffold.Services;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold;
|
||||
|
||||
public class ScaffoldCommandAppBuilder(string[] args)
|
||||
{
|
||||
private readonly string[] _args = args;
|
||||
//try to update this every release
|
||||
private readonly string _backupDotNetScaffoldVersion = "0.1.0-dev";
|
||||
|
||||
public ScaffoldCommandApp Build()
|
||||
{
|
||||
var serviceRegistrations = GetDefaultServices();
|
||||
var commandApp = new CommandApp<ScaffoldCommand>(serviceRegistrations);
|
||||
commandApp.Configure(config =>
|
||||
{
|
||||
config
|
||||
.SetApplicationName("dotnet-scaffold")
|
||||
.SetApplicationVersion(GetToolVersion());
|
||||
});
|
||||
|
||||
return new ScaffoldCommandApp(commandApp, _args);
|
||||
}
|
||||
|
||||
private ITypeRegistrar? GetDefaultServices()
|
||||
{
|
||||
var registrar = new TypeRegistrar();
|
||||
registrar.Register(typeof(IFileSystem), typeof(FileSystem));
|
||||
registrar.Register(typeof(IEnvironmentService), typeof(EnvironmentService));
|
||||
registrar.Register(typeof(IFlowProvider), typeof(FlowProvider));
|
||||
registrar.Register(typeof(IDotNetToolService), typeof(DotNetToolService));
|
||||
registrar.Register(typeof(ILogger), typeof(AnsiConsoleLogger));
|
||||
registrar.Register(typeof(IAppSettings), typeof(AppSettings));
|
||||
registrar.Register(typeof(IEnvironmentVariableProvider), typeof(MacMsbuildEnvironmentVariableProvider));
|
||||
registrar.Register(typeof(IEnvironmentVariableProvider), typeof(WindowsEnvironmentVariableProvider));
|
||||
registrar.Register(typeof(IHostService), typeof(HostService));
|
||||
return registrar;
|
||||
}
|
||||
|
||||
private string GetToolVersion()
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var assemblyAttr = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
|
||||
return assemblyAttr?.InformationalVersion ?? _backupDotNetScaffoldVersion;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.DotNet.Tools.Scaffold.AspNet.AppBuilder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.AppBuilder;
|
||||
|
||||
internal sealed class TypeRegistrar : ITypeRegistrar
|
||||
{
|
||||
private readonly IServiceCollection _services;
|
||||
|
||||
public TypeRegistrar()
|
||||
{
|
||||
_services = new ServiceCollection();
|
||||
}
|
||||
|
||||
public ITypeResolver Build()
|
||||
{
|
||||
return new TypeResolver(_services.BuildServiceProvider());
|
||||
}
|
||||
|
||||
public void Register(Type service, Type implementation)
|
||||
{
|
||||
_services.AddSingleton(service, implementation);
|
||||
}
|
||||
|
||||
public void RegisterInstance(Type service, object implementation)
|
||||
{
|
||||
_services.AddSingleton(service, implementation);
|
||||
}
|
||||
|
||||
public void RegisterLazy(Type service, Func<object> func)
|
||||
{
|
||||
if (func is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(func));
|
||||
}
|
||||
|
||||
_services.AddSingleton(service, (provider) => func());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Spectre.Console.Cli;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.AspNet.AppBuilder;
|
||||
|
||||
internal sealed class TypeResolver : ITypeResolver, IDisposable
|
||||
{
|
||||
private readonly IServiceProvider _provider;
|
||||
|
||||
public TypeResolver(IServiceProvider serviceProvider)
|
||||
{
|
||||
_provider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
}
|
||||
|
||||
public object? Resolve(Type? type)
|
||||
{
|
||||
if (type is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _provider.GetService(type);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_provider is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.Tools.Scaffold.Flow;
|
||||
using Microsoft.DotNet.Tools.Scaffold.Services;
|
||||
using Spectre.Console.Cli;
|
||||
using Spectre.Console.Flow;
|
||||
|
||||
namespace Microsoft.UpgradeAssistant.Cli.Commands;
|
||||
|
||||
public abstract class BaseCommand<TSettings> : AsyncCommand<TSettings>
|
||||
where TSettings : CommandSettings
|
||||
{
|
||||
protected BaseCommand(IFlowProvider flowProvider)
|
||||
{
|
||||
FlowProvider = flowProvider;
|
||||
}
|
||||
|
||||
protected IFlowProvider FlowProvider { get; }
|
||||
|
||||
protected async ValueTask<int> RunFlowAsync(IEnumerable<IFlowStep> flowSteps, TSettings settings, IRemainingArguments remainingArgs, bool nonInteractive = false, bool showSelectedOptions = true)
|
||||
{
|
||||
var properties = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ FlowContextProperties.RemainingArgs, remainingArgs },
|
||||
{ FlowContextProperties.CommandSettings, settings }
|
||||
};
|
||||
|
||||
IFlow? flow = null;
|
||||
Exception? exception = null;
|
||||
|
||||
try
|
||||
{
|
||||
flow = FlowProvider.GetFlow(flowSteps, properties, nonInteractive, showSelectedOptions);
|
||||
return await flow.RunAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
catch (Exception) {}
|
||||
|
||||
return exception is not null
|
||||
? throw exception
|
||||
: int.MinValue;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
|
||||
using Microsoft.DotNet.Tools.Scaffold.Flow.Steps;
|
||||
using Microsoft.DotNet.Tools.Scaffold.Flow.Steps.Project;
|
||||
using Microsoft.DotNet.Tools.Scaffold.Services;
|
||||
using Microsoft.UpgradeAssistant.Cli.Commands;
|
||||
using Spectre.Console.Cli;
|
||||
using Spectre.Console.Flow;
|
||||
|
||||
public class ScaffoldCommand : BaseCommand<ScaffoldCommand.Settings>
|
||||
{
|
||||
private readonly IAppSettings _appSettings;
|
||||
private readonly IDotNetToolService _dotnetToolService;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IHostService _hostService;
|
||||
public ScaffoldCommand(
|
||||
IAppSettings appSettings,
|
||||
IDotNetToolService dotnetToolService,
|
||||
IEnvironmentService environmentService,
|
||||
IFileSystem fileSystem,
|
||||
IFlowProvider flowProvider,
|
||||
IHostService hostService,
|
||||
ILogger logger)
|
||||
: base(flowProvider)
|
||||
{
|
||||
_appSettings = appSettings;
|
||||
_dotnetToolService = dotnetToolService;
|
||||
_environmentService = environmentService;
|
||||
_fileSystem = fileSystem;
|
||||
_hostService = hostService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public class Settings : CommandSettings
|
||||
{
|
||||
[CommandArgument(0, "[COMPONENT]")]
|
||||
public string? ComponentName { get; set; }
|
||||
|
||||
[CommandArgument(1, "[COMMAND NAME]")]
|
||||
public string? CommandName { get; set; }
|
||||
|
||||
[CommandOption("--project")]
|
||||
public string? Project { get; init; }
|
||||
|
||||
[CommandOption("--non-interactive")]
|
||||
public bool NonInteractive { get; init; }
|
||||
}
|
||||
|
||||
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
|
||||
{
|
||||
IEnumerable<IFlowStep> flowSteps =
|
||||
[
|
||||
new StartupFlowStep(_appSettings, _dotnetToolService, _environmentService, _fileSystem, _hostService, _logger),
|
||||
new SourceProjectFlowStep(_appSettings, _environmentService, _fileSystem, _logger),
|
||||
new CommandPickerFlowStep(_logger, _dotnetToolService),
|
||||
new CommandExecuteFlowStep()
|
||||
];
|
||||
|
||||
return await RunFlowAsync(flowSteps, settings, context.Remaining, settings.NonInteractive);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.Flow;
|
||||
|
||||
internal static class FlowContextProperties
|
||||
{
|
||||
public const string CommandSettings = nameof(CommandSettings);
|
||||
public const string CommandInfos = nameof(CommandInfos);
|
||||
public const string Controller = nameof(Controller);
|
||||
public const string ExcludedProjects = nameof(ExcludedProjects);
|
||||
public const string OriginalTraits = nameof(OriginalTraits);
|
||||
public const string ProjectTemplateName = nameof(ProjectTemplateName);
|
||||
public const string Slice = nameof(Slice);
|
||||
public const string SourceProjectPath = nameof(SourceProjectPath);
|
||||
public const string SourceProjectDisplay = "Source project";
|
||||
public const string CommandArgs = nameof(CommandArgs);
|
||||
public const string CommandArgValues = nameof(CommandArgValues);
|
||||
public const string ComponentName = nameof(ComponentName);
|
||||
public const string ComponentObj = nameof(ComponentObj);
|
||||
public const string CommandName = nameof(CommandName);
|
||||
public const string CommandObj = nameof(CommandObj);
|
||||
public const string ComponentNameDisplay = "Component";
|
||||
//of type Microsoft.DotNet.Scaffolding.Helpers.Services.IProjectService
|
||||
public const string SourceProject = nameof(SourceProject);
|
||||
//of type Microsoft.DotNet.Scaffolding.Helpers.Services.ICodeService
|
||||
public const string CodeService = nameof(CodeService);
|
||||
public const string TargetFrameworkName = nameof(TargetFrameworkName);
|
||||
public const string RemainingArgs = nameof(RemainingArgs);
|
||||
public const string DotnetToolComponents = nameof(DotnetToolComponents);
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.DotNet.Scaffolding.ComponentModel;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Cli;
|
||||
using Spectre.Console.Flow;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.Flow
|
||||
{
|
||||
internal static class FlowContextExtensions
|
||||
{
|
||||
public static DotNetToolInfo? GetComponentObj(this IFlowContext context, bool throwIfEmpty = false)
|
||||
{
|
||||
return context.GetValueOrThrow<DotNetToolInfo>(FlowContextProperties.ComponentObj, throwIfEmpty);
|
||||
}
|
||||
|
||||
public static CommandInfo? GetCommandObj(this IFlowContext context, bool throwIfEmpty = false)
|
||||
{
|
||||
return context.GetValueOrThrow<CommandInfo>(FlowContextProperties.CommandObj, throwIfEmpty);
|
||||
}
|
||||
|
||||
public static List<string>? GetCommandArgValues(this IFlowContext context, bool throwIfEmpty = false)
|
||||
{
|
||||
return context.GetValueOrThrow<List<string>>(FlowContextProperties.CommandArgValues, throwIfEmpty);
|
||||
}
|
||||
|
||||
public static IList<KeyValuePair<string, CommandInfo>>? GetCommandInfos(this IFlowContext context, bool throwIfEmpty = false)
|
||||
{
|
||||
return context.GetValueOrThrow<IList<KeyValuePair<string, CommandInfo>>>(FlowContextProperties.CommandInfos, throwIfEmpty);
|
||||
}
|
||||
|
||||
public static string? GetSourceProjectPath(this IFlowContext context, bool throwIfEmpty = false)
|
||||
{
|
||||
return context.GetValueOrThrow<string>(FlowContextProperties.SourceProjectPath, throwIfEmpty);
|
||||
}
|
||||
|
||||
public static ICodeService? GetCodeService(this IFlowContext context, bool throwIfEmpty = false)
|
||||
{
|
||||
return context.GetValueOrThrow<ICodeService>(FlowContextProperties.CodeService, throwIfEmpty);
|
||||
}
|
||||
|
||||
public static ScaffoldCommand.Settings? GetCommandSettings(this IFlowContext context, bool throwIfEmpty = false)
|
||||
{
|
||||
return context.GetValueOrThrow<ScaffoldCommand.Settings>(FlowContextProperties.CommandSettings, throwIfEmpty);
|
||||
}
|
||||
|
||||
public static IRemainingArguments? GetRemainingArgs(this IFlowContext context, bool throwIfEmpty = false)
|
||||
{
|
||||
return context.GetValueOrThrow<IRemainingArguments>(FlowContextProperties.RemainingArgs, throwIfEmpty);
|
||||
}
|
||||
|
||||
public static IDictionary<string, List<string>>? GetArgsDict(this IFlowContext context, bool throwIfEmpty = false)
|
||||
{
|
||||
return context.GetValueOrThrow<IDictionary<string, List<string>>>(FlowContextProperties.CommandArgs, throwIfEmpty);
|
||||
}
|
||||
public static Status WithSpinner(this Status status)
|
||||
{
|
||||
return status
|
||||
.AutoRefresh(true)
|
||||
.Spinner(Spinner.Known.Aesthetic)
|
||||
.SpinnerStyle(Styles.Highlight);
|
||||
}
|
||||
|
||||
private static T? GetValueOrThrow<T>(this IFlowContext context, string propertyName, bool throwIfEmpty = false)
|
||||
{
|
||||
var value = context.GetValue<T>(propertyName);
|
||||
if (throwIfEmpty && value is null)
|
||||
{
|
||||
throw new ArgumentNullException(propertyName);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.DotNet.Scaffolding.ComponentModel;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Flow;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps
|
||||
{
|
||||
internal class CommandDiscovery
|
||||
{
|
||||
private readonly IDotNetToolService _dotnetToolService;
|
||||
private readonly DotNetToolInfo? _componentPicked;
|
||||
public CommandDiscovery(IDotNetToolService dotnetToolService, DotNetToolInfo? componentPicked)
|
||||
{
|
||||
_dotnetToolService = dotnetToolService;
|
||||
_componentPicked = componentPicked;
|
||||
}
|
||||
|
||||
public FlowStepState State { get; private set; }
|
||||
|
||||
public KeyValuePair<string, CommandInfo>? Discover(IFlowContext context)
|
||||
{
|
||||
var allCommands = context.GetCommandInfos();
|
||||
if (allCommands is null || allCommands.Count == 0)
|
||||
{
|
||||
allCommands = AnsiConsole
|
||||
.Status()
|
||||
.WithSpinner()
|
||||
.Start("Gathering scaffolding commands!", statusContext =>
|
||||
{
|
||||
if (_componentPicked != null)
|
||||
{
|
||||
return _dotnetToolService.GetCommands(_componentPicked.Command)?.Select(x => KeyValuePair.Create(_componentPicked.Command, x))?.ToList();
|
||||
}
|
||||
|
||||
return _dotnetToolService.GetAllCommandsParallel();
|
||||
});
|
||||
|
||||
if (allCommands is not null)
|
||||
{
|
||||
context.Set(FlowContextProperties.CommandInfos, allCommands);
|
||||
}
|
||||
}
|
||||
|
||||
return Prompt(context);
|
||||
}
|
||||
|
||||
private KeyValuePair<string, CommandInfo>? Prompt(IFlowContext context)
|
||||
{
|
||||
var allCommands = context.GetCommandInfos();
|
||||
var prompt = new FlowSelectionPrompt<KeyValuePair<string, CommandInfo>>()
|
||||
.Title("[lightseagreen]Pick a scaffolding command: [/]")
|
||||
.Converter(GetCommandInfoDisplayName)
|
||||
.AddChoices(allCommands, navigation: context.Navigation);
|
||||
|
||||
var result = prompt.Show();
|
||||
State = result.State;
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
private string GetCommandInfoDisplayName(KeyValuePair<string, CommandInfo> commandInfo)
|
||||
{
|
||||
return $"{commandInfo.Value.DisplayName} ({commandInfo.Key})";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.Scaffolding.ComponentModel;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.General;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Flow;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps
|
||||
{
|
||||
/// <summary>
|
||||
/// IFlowStep where we gather all the parameter values, and then execute the chosen/given component (dotnet tool).
|
||||
/// Print out stdout/stderr
|
||||
/// </summary>
|
||||
internal class CommandExecuteFlowStep : IFlowStep
|
||||
{
|
||||
public string Id => nameof(CommandExecuteFlowStep);
|
||||
|
||||
public string DisplayName => "Command Execute";
|
||||
|
||||
public ValueTask ResetAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask<FlowStepResult> RunAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
|
||||
}
|
||||
|
||||
public ValueTask<FlowStepResult> ValidateUserInputAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
//need all 3 things, throw if not found
|
||||
var componentName = context.GetComponentObj()?.Command;
|
||||
var commandObj = context.GetCommandObj();
|
||||
if (commandObj is null || string.IsNullOrEmpty(componentName))
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Failure("Missing value for name of the component and/or command"));
|
||||
}
|
||||
|
||||
var parameterValues = GetAllParameterValues(context, commandObj);
|
||||
if (!string.IsNullOrEmpty(componentName) && parameterValues.Count != 0 && !string.IsNullOrEmpty(commandObj.Name))
|
||||
{
|
||||
var componentExecutionString = $"{componentName} {commandObj.Name} {string.Join(" ", parameterValues)}";
|
||||
string? stdOut = null, stdErr = null;
|
||||
int? exitCode = null;
|
||||
AnsiConsole.Status().WithSpinner()
|
||||
.Start($"Executing '{componentExecutionString}'", statusContext =>
|
||||
{
|
||||
var cliRunner = DotnetCliRunner.Create(componentName, parameterValues);
|
||||
exitCode = cliRunner.ExecuteAndCaptureOutput(out stdOut, out stdErr);
|
||||
});
|
||||
|
||||
if (exitCode != null && (!string.IsNullOrEmpty(stdOut) || string.IsNullOrEmpty(stdErr)))
|
||||
{
|
||||
AnsiConsole.Console.WriteLine($"\nCommand executed : '{componentExecutionString}'");
|
||||
AnsiConsole.Console.WriteLine($"\nCommand exit code - {exitCode}");
|
||||
if (exitCode == 0)
|
||||
{
|
||||
AnsiConsole.Console.MarkupLine($"\nCommand stdout :\n[lightgreen]{stdOut}[/]");
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.Console.MarkupLine($"\nCommand stderr :\n[lightred]{stdErr}[/]");
|
||||
}
|
||||
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
|
||||
}
|
||||
}
|
||||
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Failure());
|
||||
}
|
||||
|
||||
private List<string> GetAllParameterValues(IFlowContext context, CommandInfo commandInfo)
|
||||
{
|
||||
var sourceProjectPath = context.GetSourceProjectPath();
|
||||
var parameterValues = new List<string> { commandInfo.Name };
|
||||
if (!string.IsNullOrEmpty(sourceProjectPath))
|
||||
{
|
||||
parameterValues.Add("--project");
|
||||
parameterValues.Add(sourceProjectPath);
|
||||
}
|
||||
|
||||
foreach (var parameter in commandInfo.Parameters)
|
||||
{
|
||||
var parameterValue = context.GetValue<string>(parameter.Name);
|
||||
if (!string.IsNullOrEmpty(parameterValue))
|
||||
{
|
||||
parameterValues.Add(parameter.Name);
|
||||
parameterValues.Add(parameterValue);
|
||||
}
|
||||
}
|
||||
|
||||
return parameterValues;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.Scaffolding.ComponentModel;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
using Spectre.Console.Flow;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps
|
||||
{
|
||||
/// <summary>
|
||||
/// IFlowStep that deals with the selection of the component(DotnetToolInfo) and the associated command(CommandInfo).
|
||||
/// if provided by the user, verify if the component is installed and the command is supported.
|
||||
/// </summary>
|
||||
internal class CommandPickerFlowStep : IFlowStep
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDotNetToolService _dotnetToolService;
|
||||
public CommandPickerFlowStep(ILogger logger, IDotNetToolService dotnetToolService)
|
||||
{
|
||||
_logger = logger;
|
||||
_dotnetToolService = dotnetToolService;
|
||||
}
|
||||
|
||||
public string Id => nameof(CommandPickerFlowStep);
|
||||
|
||||
public string DisplayName => "Command Name";
|
||||
|
||||
public ValueTask ResetAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
context.Unset(FlowContextProperties.ComponentName);
|
||||
context.Unset(FlowContextProperties.ComponentObj);
|
||||
context.Unset(FlowContextProperties.CommandName);
|
||||
context.Unset(FlowContextProperties.CommandObj);
|
||||
return new ValueTask();
|
||||
}
|
||||
|
||||
public ValueTask<FlowStepResult> RunAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = context.GetCommandSettings();
|
||||
var componentName = settings?.ComponentName;
|
||||
var commandName = settings?.CommandName;
|
||||
//KeyValuePair with key being name of the DotnetToolInfo (component) and value being the CommandInfo supported by that component.
|
||||
KeyValuePair<string, CommandInfo>? commandInfoKvp = null;
|
||||
CommandInfo? commandInfo = null;
|
||||
var dotnetToolComponent = _dotnetToolService.GlobalDotNetTools.FirstOrDefault(x => x.Command.Equals(componentName, StringComparison.OrdinalIgnoreCase));
|
||||
CommandDiscovery commandDiscovery = new(_dotnetToolService, dotnetToolComponent);
|
||||
commandInfoKvp = commandDiscovery.Discover(context);
|
||||
if (commandDiscovery.State.IsNavigation())
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(new FlowStepResult { State = commandDiscovery.State });
|
||||
}
|
||||
|
||||
if (commandInfoKvp is null || !commandInfoKvp.HasValue || commandInfoKvp.Value.Value is null || string.IsNullOrEmpty(commandInfoKvp.Value.Key))
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Failure("Unable to find any commands!"));
|
||||
}
|
||||
else
|
||||
{
|
||||
commandInfo = commandInfoKvp.Value.Value;
|
||||
componentName = commandInfoKvp.Value.Key;
|
||||
dotnetToolComponent ??= _dotnetToolService.GetDotNetTool(componentName);
|
||||
if (dotnetToolComponent != null)
|
||||
{
|
||||
SelectComponent(context, dotnetToolComponent);
|
||||
}
|
||||
|
||||
SelectCommand(context, commandInfo);
|
||||
}
|
||||
|
||||
var commandFirstStep = GetFirstParameterBasedStep(commandInfo);
|
||||
if (commandFirstStep is null)
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Failure($"Failed to get/parse parameters for command '{commandInfo.Name}'"));
|
||||
}
|
||||
|
||||
return new ValueTask<FlowStepResult>(new FlowStepResult { State = FlowStepState.Success, Steps = new List<ParameterBasedFlowStep> { commandFirstStep } });
|
||||
}
|
||||
|
||||
public ValueTask<FlowStepResult> ValidateUserInputAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = context.GetCommandSettings();
|
||||
var componentName = settings?.ComponentName;
|
||||
var commandName = settings?.CommandName;
|
||||
CommandInfo? commandInfo = null;
|
||||
|
||||
//check if user input included a component name.
|
||||
//if included, check for a command name, and get the CommandInfo object.
|
||||
var dotnetToolComponent = _dotnetToolService.GlobalDotNetTools.FirstOrDefault(x => x.Command.Equals(componentName, StringComparison.OrdinalIgnoreCase));
|
||||
if (dotnetToolComponent != null)
|
||||
{
|
||||
var allCommands = _dotnetToolService.GetCommands(dotnetToolComponent.Command);
|
||||
commandInfo = allCommands.FirstOrDefault(x => x.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Failure("No component (dotnet tool) provided!"));
|
||||
}
|
||||
|
||||
if (commandInfo is null)
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Failure($"Invalid or empty command provided for component '{componentName}'"));
|
||||
}
|
||||
|
||||
var commandFirstStep = GetFirstParameterBasedStep(commandInfo);
|
||||
if (commandFirstStep is null)
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Failure($"Failed to get/parse parameters for command '{commandInfo.Name}'"));
|
||||
}
|
||||
|
||||
SelectComponent(context, dotnetToolComponent);
|
||||
SelectCommand(context, commandInfo);
|
||||
return new ValueTask<FlowStepResult>(new FlowStepResult { State = FlowStepState.Success, Steps = new List<ParameterBasedFlowStep> { commandFirstStep } });
|
||||
}
|
||||
|
||||
//Wrapper to get the first ParameterBasedFlowStep. Use 'BuildParameterFlowSteps'
|
||||
internal ParameterBasedFlowStep? GetFirstParameterBasedStep(CommandInfo commandInfo)
|
||||
{
|
||||
ParameterBasedFlowStep? firstParameterStep = null;
|
||||
if (commandInfo.Parameters != null && commandInfo.Parameters.Length != 0)
|
||||
{
|
||||
firstParameterStep = BuildParameterFlowSteps([.. commandInfo.Parameters]);
|
||||
}
|
||||
|
||||
return firstParameterStep;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Take all the 'Parameter's, create ParameterBasedFlowSteps with connecting them using 'NextStep'.
|
||||
/// </summary>
|
||||
/// <returns>first step from the connected ParameterBasedFlowSteps</returns>
|
||||
internal ParameterBasedFlowStep? BuildParameterFlowSteps(List<Parameter> parameters)
|
||||
{
|
||||
ParameterBasedFlowStep? firstStep = null;
|
||||
ParameterBasedFlowStep? previousStep = null;
|
||||
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
var step = new ParameterBasedFlowStep(parameter, null);
|
||||
if (firstStep == null)
|
||||
{
|
||||
// This is the first step
|
||||
firstStep = step;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (previousStep != null)
|
||||
{
|
||||
// Connect the previous step to this step
|
||||
previousStep.NextStep = step;
|
||||
}
|
||||
}
|
||||
|
||||
previousStep = step;
|
||||
}
|
||||
|
||||
return firstStep;
|
||||
}
|
||||
|
||||
private void SelectCommand(IFlowContext context, CommandInfo command)
|
||||
{
|
||||
context.Set(new FlowProperty(
|
||||
FlowContextProperties.CommandName,
|
||||
command.Name,
|
||||
"Command Name",
|
||||
isVisible: true));
|
||||
|
||||
context.Set(new FlowProperty(
|
||||
FlowContextProperties.CommandObj,
|
||||
command,
|
||||
isVisible: false));
|
||||
}
|
||||
|
||||
private void SelectComponent(IFlowContext context, DotNetToolInfo dotnetToolInfo)
|
||||
{
|
||||
context.Set(new FlowProperty(
|
||||
FlowContextProperties.ComponentName,
|
||||
dotnetToolInfo.Command,
|
||||
"Component Name",
|
||||
isVisible: true));
|
||||
|
||||
context.Set(new FlowProperty(
|
||||
FlowContextProperties.ComponentObj,
|
||||
dotnetToolInfo,
|
||||
isVisible: false));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.DotNet.Scaffolding.ComponentModel;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
using Spectre.Console.Flow;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps
|
||||
{
|
||||
internal class ComponentDiscovery
|
||||
{
|
||||
private readonly IDotNetToolService _dotnetToolService;
|
||||
|
||||
public ComponentDiscovery(IDotNetToolService dotNetToolService)
|
||||
{
|
||||
_dotnetToolService = dotNetToolService;
|
||||
}
|
||||
public FlowStepState State { get; private set; }
|
||||
|
||||
public DotNetToolInfo? Discover(IFlowContext context)
|
||||
{
|
||||
return Prompt(context, "Pick a scaffolding component ('dotnet tool')", _dotnetToolService.GlobalDotNetTools);
|
||||
}
|
||||
|
||||
private DotNetToolInfo? Prompt(IFlowContext context, string title, IList<DotNetToolInfo> components)
|
||||
{
|
||||
if (components.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (components.Count == 1)
|
||||
{
|
||||
return components[0];
|
||||
}
|
||||
|
||||
var prompt = new FlowSelectionPrompt<DotNetToolInfo>()
|
||||
.Title(title)
|
||||
.Converter(GetDotNetToolInfoDisplayString)
|
||||
.AddChoices(components, navigation: context.Navigation);
|
||||
|
||||
var result = prompt.Show();
|
||||
State = result.State;
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
internal string GetDotNetToolInfoDisplayString(DotNetToolInfo dotnetToolInfo)
|
||||
{
|
||||
return dotnetToolInfo.ToDisplayString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.Scaffolding.ComponentModel;
|
||||
using Spectre.Console.Flow;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps
|
||||
{
|
||||
internal class ParameterBasedFlowStep : IFlowStep
|
||||
{
|
||||
public Parameter Parameter { get; set; }
|
||||
public ParameterBasedFlowStep? NextStep { get; set; }
|
||||
public ParameterBasedFlowStep(Parameter parameter, ParameterBasedFlowStep? nextStep)
|
||||
{
|
||||
Parameter = parameter;
|
||||
NextStep = nextStep;
|
||||
}
|
||||
|
||||
public string Id => nameof(ParameterBasedFlowStep);
|
||||
public string DisplayName => Parameter.DisplayName;
|
||||
|
||||
public ValueTask ResetAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
context.Unset(Parameter.Name);
|
||||
return new ValueTask();
|
||||
}
|
||||
|
||||
public async ValueTask<FlowStepResult> RunAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
ParameterDiscovery paraDiscovery = new ParameterDiscovery(Parameter);
|
||||
var parameterValue = await paraDiscovery.DiscoverAsync(context);
|
||||
if (string.Equals(parameterValue, FlowNavigation.BackInputToken, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return FlowStepResult.Back;
|
||||
}
|
||||
else if (paraDiscovery.State.IsNavigation())
|
||||
{
|
||||
return new FlowStepResult { State = paraDiscovery.State };
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectParameter(context, parameterValue ?? string.Empty);
|
||||
}
|
||||
|
||||
if (NextStep != null)
|
||||
{
|
||||
return new FlowStepResult { State = FlowStepState.Success, Steps = new List<ParameterBasedFlowStep> { NextStep } };
|
||||
}
|
||||
else
|
||||
{
|
||||
return FlowStepResult.Success;
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask<FlowStepResult> ValidateUserInputAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
//check for parameter in the context using --name
|
||||
var cmdlineArgs = context.GetArgsDict();
|
||||
var commandSettings = context.GetCommandSettings();
|
||||
if (cmdlineArgs != null && cmdlineArgs.Count != 0)
|
||||
{
|
||||
var strippedParameterName = Parameter.Name.Replace("--", string.Empty);
|
||||
cmdlineArgs.TryGetValue(strippedParameterName, out var cmdlineValues);
|
||||
if (cmdlineValues != null && cmdlineValues.Count != 0 && ParameterHelpers.CheckType(Parameter.Type, cmdlineValues.First()))
|
||||
{
|
||||
SelectParameter(context, cmdlineValues.First());
|
||||
if (NextStep != null)
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(new FlowStepResult { State = FlowStepState.Success, Steps = new List<ParameterBasedFlowStep> { NextStep } });
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Parameter.Required || (commandSettings != null && !commandSettings.NonInteractive))
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Failure($"No value found for option '{Parameter.Name}'"));
|
||||
}
|
||||
else if (NextStep != null)
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(new FlowStepResult { State = FlowStepState.Success, Steps = new List<ParameterBasedFlowStep> { NextStep } });
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectParameter(IFlowContext context, string parameterValue)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(parameterValue))
|
||||
{
|
||||
context.Set(new FlowProperty(
|
||||
Parameter.Name,
|
||||
parameterValue,
|
||||
Parameter.DisplayName,
|
||||
isVisible: true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.DotNet.Scaffolding.ComponentModel;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Flow;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps
|
||||
{
|
||||
internal class ParameterDiscovery
|
||||
{
|
||||
private readonly Parameter _parameter;
|
||||
public ParameterDiscovery(Parameter parameter)
|
||||
{
|
||||
_parameter = parameter;
|
||||
}
|
||||
public FlowStepState State { get; private set; }
|
||||
|
||||
public async Task<string> DiscoverAsync(IFlowContext context)
|
||||
{
|
||||
var optionParameterAddition = _parameter.Required ? "(" : "(empty to skip, ";
|
||||
return await PromptAsync(context, $"Enter new value for '{_parameter.DisplayName}' {optionParameterAddition}[sandybrown]<[/] to go back) : ");
|
||||
}
|
||||
|
||||
private async Task<string> PromptAsync(IFlowContext context, string title)
|
||||
{
|
||||
//check if Parameter has a InteractivePickerType
|
||||
if (_parameter.PickerType is null)
|
||||
{
|
||||
var prompt = new TextPrompt<string>($"[lightseagreen]{title}[/]")
|
||||
.ValidationErrorMessage("bad value fix it please")
|
||||
.Validate(x =>
|
||||
{
|
||||
if (x.Trim() == FlowNavigation.BackInputToken)
|
||||
{
|
||||
return ValidationResult.Success();
|
||||
}
|
||||
|
||||
return Validate(context, x);
|
||||
})
|
||||
.AllowEmpty();
|
||||
|
||||
await Task.Delay(1);
|
||||
return AnsiConsole.Prompt(prompt).Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
var codeService = context.GetCodeService();
|
||||
if (codeService != null)
|
||||
{
|
||||
return await PromptInteractivePicker(context, _parameter.PickerType, codeService) ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private async Task<string?> PromptInteractivePicker(IFlowContext context, InteractivePickerType? pickerType, ICodeService codeService)
|
||||
{
|
||||
IList<Tuple<string, string>>? displayTuples = [];
|
||||
string interactiveTitle = string.Empty;
|
||||
switch (pickerType)
|
||||
{
|
||||
case InteractivePickerType.ClassPicker:
|
||||
var allClassSymbols = await AnsiConsole
|
||||
.Status()
|
||||
.WithSpinner()
|
||||
.Start("Gathering project classes!", async statusContext =>
|
||||
{
|
||||
return (await codeService.GetAllClassSymbolsAsync()).ToList();
|
||||
});
|
||||
|
||||
displayTuples = GetClassDisplayNames(allClassSymbols);
|
||||
interactiveTitle = "Class";
|
||||
break;
|
||||
case InteractivePickerType.FilePicker:
|
||||
var allDocuments = (await codeService.GetAllDocumentsAsync()).ToList();
|
||||
displayTuples = GetDocumentNames(allDocuments);
|
||||
interactiveTitle = "File";
|
||||
break;
|
||||
case InteractivePickerType.DbProviderPicker:
|
||||
displayTuples = DbProviders;
|
||||
interactiveTitle = "DbProvider";
|
||||
break;
|
||||
}
|
||||
|
||||
if (!_parameter.Required)
|
||||
{
|
||||
displayTuples.Insert(0, Tuple.Create("None", string.Empty));
|
||||
}
|
||||
|
||||
var prompt = new FlowSelectionPrompt<Tuple<string, string>>()
|
||||
.Title($"[lightseagreen]Pick a {interactiveTitle}: [/]")
|
||||
.Converter(GetDisplayNameFromTuple)
|
||||
.AddChoices(displayTuples, navigation: context.Navigation);
|
||||
|
||||
var result = prompt.Show();
|
||||
State = result.State;
|
||||
return result.Value?.Item2;
|
||||
}
|
||||
|
||||
private string GetDisplayNameFromTuple(Tuple<string, string> tuple)
|
||||
{
|
||||
bool displayNone = tuple.Item1.Equals("None", StringComparison.OrdinalIgnoreCase);
|
||||
return displayNone ? $"[sandybrown]{tuple.Item1} (empty to skip parameter)[/]" : $"{tuple.Item1} ({tuple.Item2})";
|
||||
}
|
||||
|
||||
private ValidationResult Validate(IFlowContext context, string promptVal)
|
||||
{
|
||||
if (!_parameter.Required && string.IsNullOrEmpty(promptVal))
|
||||
{
|
||||
return ValidationResult.Success();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(promptVal) && !ParameterHelpers.CheckType(_parameter.Type, promptVal))
|
||||
{
|
||||
return ValidationResult.Error("Invalid input, please try again!");
|
||||
}
|
||||
|
||||
return ValidationResult.Success();
|
||||
}
|
||||
|
||||
private IList<Tuple<string, string>> GetClassDisplayNames(List<ISymbol> compilationClassSymbols)
|
||||
{
|
||||
List<Tuple<string, string>> classNames = [];
|
||||
if (compilationClassSymbols != null && compilationClassSymbols.Count != 0)
|
||||
{
|
||||
compilationClassSymbols.ForEach(
|
||||
x =>
|
||||
{
|
||||
if (x != null)
|
||||
{
|
||||
classNames.Add(Tuple.Create(x.MetadataName, x.Name));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return classNames;
|
||||
}
|
||||
|
||||
private List<Tuple<string, string>> GetDocumentNames(List<Document> documents)
|
||||
{
|
||||
List<Tuple<string, string>> classNames = [];
|
||||
if (documents != null && documents.Count != 0)
|
||||
{
|
||||
documents.ForEach(
|
||||
x =>
|
||||
{
|
||||
if (x != null)
|
||||
{
|
||||
string fileName = System.IO.Path.GetFileName(x.Name);
|
||||
classNames.Add(Tuple.Create(fileName, x.Name));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return classNames;
|
||||
}
|
||||
|
||||
private static List<Tuple<string, string>>? _dbProviders;
|
||||
private static List<Tuple<string, string>> DbProviders
|
||||
{
|
||||
get
|
||||
{
|
||||
_dbProviders ??=
|
||||
[
|
||||
Tuple.Create("SQL Server", "sqlserver"),
|
||||
Tuple.Create("SQLite", "sqlite"),
|
||||
Tuple.Create("PostgreSQL", "postgres"),
|
||||
Tuple.Create("Cosmos DB", "cosmos")
|
||||
];
|
||||
|
||||
return _dbProviders;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Extensions;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Flow;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps.Project
|
||||
{
|
||||
internal class ProjectDiscovery
|
||||
{
|
||||
public ProjectDiscovery(IFileSystem fileSystem, string workingDir)
|
||||
{
|
||||
FileSystem = fileSystem;
|
||||
WorkingDir = workingDir;
|
||||
}
|
||||
|
||||
protected IFileSystem FileSystem { get; }
|
||||
protected string WorkingDir { get; set; }
|
||||
public FlowStepState State { get; private set; }
|
||||
public string? Discover(IFlowContext context, string path)
|
||||
{
|
||||
List<string> projects = [];
|
||||
projects = AnsiConsole
|
||||
.Status()
|
||||
.WithSpinner()
|
||||
.Start("Discovering project files!", statusContext =>
|
||||
{
|
||||
if (!FileSystem.DirectoryExists(path))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var projects = FileSystem.EnumerateFiles(path, "*.csproj", SearchOption.AllDirectories).ToList();
|
||||
var slnFiles = FileSystem.EnumerateFiles(path, "*.sln", SearchOption.TopDirectoryOnly).ToList();
|
||||
var projectsFromSlnFiles = GetProjectsFromSolutionFiles(slnFiles, WorkingDir);
|
||||
projects = projects.Union(projectsFromSlnFiles).ToList();
|
||||
|
||||
//should we search in all directories if nothing in top level? (yes, for now)
|
||||
if (projects.Count == 0)
|
||||
{
|
||||
projects = FileSystem.EnumerateFiles(path, "*.csproj", SearchOption.AllDirectories).ToList();
|
||||
}
|
||||
|
||||
return projects;
|
||||
});
|
||||
|
||||
return Prompt(context, $"[lightseagreen]Pick project ({projects.Count}): [/]", projects);
|
||||
}
|
||||
|
||||
internal IList<string> GetProjectsFromSolutionFiles(List<string> solutionFiles, string workingDir)
|
||||
{
|
||||
List<string> projectPaths = new();
|
||||
foreach(var solutionFile in solutionFiles)
|
||||
{
|
||||
projectPaths.AddRange(GetProjectsFromSolutionFile(solutionFile, workingDir));
|
||||
}
|
||||
|
||||
return projectPaths;
|
||||
}
|
||||
|
||||
internal IList<string> GetProjectsFromSolutionFile(string solutionFilePath, string workingDir)
|
||||
{
|
||||
List<string> projectPaths = new();
|
||||
if (string.IsNullOrEmpty(solutionFilePath))
|
||||
{
|
||||
return projectPaths;
|
||||
}
|
||||
|
||||
string slnText = FileSystem.ReadAllText(solutionFilePath);
|
||||
string projectPattern = @"Project\(""\{[A-F0-9\-]*\}""\)\s*=\s*""([^""]*)"",\s*""([^""]*)"",\s*""\{[A-F0-9\-]*\}""";
|
||||
var matches = Regex.Matches(slnText, projectPattern);
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
string projectRelativePath = match.Groups[2].Value;
|
||||
string? solutionDirPath = Path.GetDirectoryName(solutionFilePath) ?? workingDir;
|
||||
string projectPath = Path.GetFullPath(projectRelativePath, solutionDirPath);
|
||||
projectPaths.Add(projectPath);
|
||||
}
|
||||
|
||||
return projectPaths;
|
||||
}
|
||||
|
||||
internal string GetProjectDisplayName(string projectPath)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(projectPath);
|
||||
var relativePath = projectPath.MakeRelativePath(WorkingDir).ToSuggestion();
|
||||
return $"{name} {relativePath.ToSuggestion(withBrackets: true)}";
|
||||
}
|
||||
|
||||
private string? Prompt(IFlowContext context, string title, IList<string> projectFiles)
|
||||
{
|
||||
if (projectFiles.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (projectFiles.Count == 1)
|
||||
{
|
||||
return projectFiles[0];
|
||||
}
|
||||
|
||||
var prompt = new FlowSelectionPrompt<string>()
|
||||
.Title(title)
|
||||
.Converter(GetProjectDisplayName)
|
||||
.AddChoices(projectFiles.OrderBy(x => Path.GetFileNameWithoutExtension(x)), navigation: context.Navigation);
|
||||
|
||||
var result = prompt.Show();
|
||||
State = result.State;
|
||||
return result.Value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Extensions;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
|
||||
using Spectre.Console.Flow;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps.Project;
|
||||
|
||||
internal class SourceProjectFlowStep : IFlowStep
|
||||
{
|
||||
private readonly IAppSettings _appSettings;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly ILogger _logger;
|
||||
public SourceProjectFlowStep(
|
||||
IAppSettings appSettings,
|
||||
IEnvironmentService environment,
|
||||
IFileSystem fileSystem,
|
||||
ILogger logger)
|
||||
{
|
||||
_appSettings = appSettings;
|
||||
_fileSystem = fileSystem;
|
||||
_environmentService = environment;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Id => nameof(SourceProjectFlowStep);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName => "Source Project";
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<FlowStepResult> ValidateUserInputAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var projectPath = context.GetSourceProjectPath();
|
||||
if (string.IsNullOrEmpty(projectPath))
|
||||
{
|
||||
var settings = context.GetCommandSettings();
|
||||
projectPath = settings?.Project;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(projectPath))
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Failure("Source project is needed!"));
|
||||
}
|
||||
|
||||
if (!projectPath.IsCSharpProject())
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Failure($"Project path is invalid '{projectPath}'"));
|
||||
}
|
||||
|
||||
if (!Path.IsPathRooted(projectPath))
|
||||
{
|
||||
projectPath = Path.GetFullPath(Path.Combine(_environmentService.CurrentDirectory, projectPath.Trim(Path.DirectorySeparatorChar)));
|
||||
}
|
||||
|
||||
if (!_fileSystem.FileExists(projectPath))
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Failure(string.Format("Project file '{0}' does not exist", projectPath)));
|
||||
}
|
||||
|
||||
SelectSourceProject(context, projectPath);
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<FlowStepResult> RunAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = context.GetCommandSettings();
|
||||
var path = settings?.Project;
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
path = _environmentService.CurrentDirectory;
|
||||
}
|
||||
|
||||
if (!Path.IsPathRooted(path))
|
||||
{
|
||||
path = Path.GetFullPath(Path.Combine(_environmentService.CurrentDirectory, path.Trim(Path.DirectorySeparatorChar)));
|
||||
}
|
||||
|
||||
var workingDir = _environmentService.CurrentDirectory;
|
||||
if (path.EndsWith(".sln"))
|
||||
{
|
||||
workingDir = Path.GetDirectoryName(path)!;
|
||||
}
|
||||
else if (_fileSystem.DirectoryExists(path))
|
||||
{
|
||||
workingDir = path;
|
||||
}
|
||||
|
||||
ProjectDiscovery projectDiscovery = new(_fileSystem, workingDir);
|
||||
var projectPath = projectDiscovery.Discover(context, path);
|
||||
if (projectDiscovery.State.IsNavigation())
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(new FlowStepResult { State = projectDiscovery.State });
|
||||
}
|
||||
|
||||
if (projectPath is not null)
|
||||
{
|
||||
SelectSourceProject(context, projectPath);
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
|
||||
}
|
||||
|
||||
_logger.LogMessage("No projects found in current directory", LogMessageType.Error);
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Failure());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask ResetAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
context.Unset(FlowContextProperties.SourceProjectPath);
|
||||
context.Unset(FlowContextProperties.SourceProject);
|
||||
return new ValueTask();
|
||||
}
|
||||
|
||||
private void SelectSourceProject(IFlowContext context, string projectPath)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(projectPath))
|
||||
{
|
||||
context.Set(new FlowProperty(
|
||||
FlowContextProperties.SourceProjectPath,
|
||||
projectPath,
|
||||
FlowContextProperties.SourceProjectDisplay,
|
||||
isVisible: true));
|
||||
_appSettings.Workspace().InputPath = projectPath;
|
||||
|
||||
ICodeService codeService = new CodeService(_appSettings, _logger);
|
||||
context.Set(new FlowProperty(
|
||||
FlowContextProperties.CodeService,
|
||||
codeService,
|
||||
isVisible: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.Scaffolding.ComponentModel;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.General;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services.Environment;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Flow;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.Flow.Steps;
|
||||
/// <summary>
|
||||
/// do first time initialization in ValidateUserInputAsync
|
||||
/// - initialize MSBuild instance
|
||||
/// - gather some specific environment variables
|
||||
/// -
|
||||
/// -
|
||||
/// </summary>
|
||||
public class StartupFlowStep : IFlowStep
|
||||
{
|
||||
private readonly IAppSettings _appSettings;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IDotNetToolService _dotnetToolService;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IHostService _hostService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly bool _initializeMsbuild;
|
||||
public StartupFlowStep(
|
||||
IAppSettings appSettings,
|
||||
IDotNetToolService dotnetToolService,
|
||||
IEnvironmentService environmentService,
|
||||
IFileSystem fileSystem,
|
||||
IHostService hostService,
|
||||
ILogger logger,
|
||||
bool initializeMsbuild = true)
|
||||
{
|
||||
_appSettings = appSettings;
|
||||
_dotnetToolService = dotnetToolService;
|
||||
_environmentService = environmentService;
|
||||
_fileSystem = fileSystem;
|
||||
_hostService = hostService;
|
||||
_logger = logger;
|
||||
_initializeMsbuild = initializeMsbuild;
|
||||
}
|
||||
|
||||
public string Id => nameof(StartupFlowStep);
|
||||
|
||||
public string DisplayName => "Startup step";
|
||||
|
||||
public ValueTask ResetAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ValueTask();
|
||||
}
|
||||
|
||||
public ValueTask<FlowStepResult> RunAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
|
||||
}
|
||||
|
||||
public ValueTask<FlowStepResult> ValidateUserInputAsync(IFlowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
AnsiConsole.Status()
|
||||
.WithSpinner()
|
||||
.Start("Initializing dotnet-scaffold", async statusContext =>
|
||||
{
|
||||
statusContext.Refresh();
|
||||
//add 'workspace' settings
|
||||
var workspaceSettings = new WorkspaceSettings();
|
||||
_appSettings.AddSettings("workspace", workspaceSettings);
|
||||
|
||||
if (_initializeMsbuild)
|
||||
{
|
||||
statusContext.Status = "Initializing msbuild!";
|
||||
new MsBuildInitializer(_logger).Initialize();
|
||||
statusContext.Status = "DONE\n";
|
||||
}
|
||||
//initialize environment variables
|
||||
statusContext.Status = "Gathering environment variables!";
|
||||
var environmentVariableProvider = new EnvironmentVariablesStartup(_hostService, _environmentService, _appSettings);
|
||||
await environmentVariableProvider.StartupAsync();
|
||||
statusContext.Status = "DONE\n";
|
||||
|
||||
//parse args passed
|
||||
statusContext.Status = "Parsing args!";
|
||||
var remainingArgs = context.GetRemainingArgs();
|
||||
if (remainingArgs != null)
|
||||
{
|
||||
var argDict = CliHelpers.ParseILookup(remainingArgs.Parsed);
|
||||
if (argDict != null)
|
||||
{
|
||||
SelectCommandArgs(context, argDict);
|
||||
}
|
||||
}
|
||||
|
||||
statusContext.Status = "DONE\n";
|
||||
});
|
||||
|
||||
return new ValueTask<FlowStepResult>(FlowStepResult.Success);
|
||||
}
|
||||
|
||||
private void SelectCommandArgs(IFlowContext context, IDictionary<string, List<string>> args)
|
||||
{
|
||||
if (args != null)
|
||||
{
|
||||
context.Set(new FlowProperty(
|
||||
FlowContextProperties.CommandArgs,
|
||||
args,
|
||||
isVisible: false));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,489 +1,7 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
using Microsoft.DotNet.Tools.Scaffold;
|
||||
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Parsing;
|
||||
using Microsoft.DotNet.MSIdentity.Tool;
|
||||
using MsIdentity = Microsoft.DotNet.MSIdentity.Tool.Program;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
private const string SCAFFOLD_COMMAND = "scaffold";
|
||||
private const string AREA_COMMAND = "--area";
|
||||
private const string CONTROLLER_COMMAND = "--controller";
|
||||
private const string IDENTITY_COMMAND = "--identity";
|
||||
private const string RAZORPAGE_COMMAND = "--razorpage";
|
||||
private const string VIEW_COMMAND = "--view";
|
||||
/*
|
||||
dotnet scaffold [generator] [-p|--project] [-n|--nuget-package-dir] [-c|--configuration] [-tfm|--target-framework] [-b|--build-base-path] [--no-build]
|
||||
|
||||
This commands supports the following generators :
|
||||
Area
|
||||
Controller
|
||||
Identity
|
||||
Razorpage
|
||||
View
|
||||
|
||||
e.g: dotnet scaffold area <AreaNameToGenerate>
|
||||
dotnet scaffold identity
|
||||
dotnet scaffold razorpage
|
||||
|
||||
*/
|
||||
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
var rootCommand = ScaffoldCommand();
|
||||
|
||||
rootCommand.AddCommand(ScaffoldAreaCommand());
|
||||
rootCommand.AddCommand(ScaffoldControllerCommand());
|
||||
rootCommand.AddCommand(ScaffoldRazorPageCommand());
|
||||
rootCommand.AddCommand(ScaffoldViewCommand());
|
||||
rootCommand.AddCommand(ScaffoldIdentityCommand());
|
||||
//msidentity commands
|
||||
//new BinderBase for System.Commandline update, new way to bind handlers to commands.
|
||||
var provisioningToolBinder = new ProvisioningToolOptionsBinder(
|
||||
MsIdentity.JsonOption,
|
||||
MsIdentity.EnableIdTokenOption,
|
||||
MsIdentity.EnableAccessToken,
|
||||
MsIdentity.CallsGraphOption,
|
||||
MsIdentity.CallsDownstreamApiOption,
|
||||
MsIdentity.UpdateUserSecretsOption,
|
||||
MsIdentity.ConfigUpdateOption,
|
||||
MsIdentity.CodeUpdateOption,
|
||||
MsIdentity.PackagesUpdateOption,
|
||||
MsIdentity.ClientIdOption,
|
||||
MsIdentity.AppDisplayName,
|
||||
MsIdentity.ProjectType,
|
||||
MsIdentity.ClientSecretOption,
|
||||
MsIdentity.RedirectUriOption,
|
||||
MsIdentity.ProjectFilePathOption,
|
||||
MsIdentity.ClientProjectOption,
|
||||
MsIdentity.ApiScopesOption,
|
||||
MsIdentity.HostedAppIdUriOption,
|
||||
MsIdentity.ApiClientIdOption,
|
||||
MsIdentity.SusiPolicyIdOption,
|
||||
MsIdentity.TenantOption,
|
||||
MsIdentity.UsernameOption,
|
||||
MsIdentity.InstanceOption,
|
||||
MsIdentity.CalledApiUrlOption);
|
||||
|
||||
//internal commands
|
||||
var listAadAppsCommand = MsIdentity.ListAADAppsCommand();
|
||||
var listServicePrincipalsCommand = MsIdentity.ListServicePrincipalsCommand();
|
||||
var listTenantsCommand = MsIdentity.ListTenantsCommand();
|
||||
var createClientSecretCommand = MsIdentity.CreateClientSecretCommand();
|
||||
|
||||
//exposed commands
|
||||
var registerApplicationCommand = MsIdentity.RegisterApplicationCommand();
|
||||
var unregisterApplicationCommand = MsIdentity.UnregisterApplicationCommand();
|
||||
var updateAppRegistrationCommand = MsIdentity.UpdateAppRegistrationCommand();
|
||||
var updateProjectCommand = MsIdentity.UpdateProjectCommand();
|
||||
var createAppRegistration = MsIdentity.CreateAppRegistrationCommand();
|
||||
|
||||
//hide internal commands.
|
||||
listAadAppsCommand.IsHidden = true;
|
||||
listServicePrincipalsCommand.IsHidden = true;
|
||||
listTenantsCommand.IsHidden = true;
|
||||
updateProjectCommand.IsHidden = true;
|
||||
createClientSecretCommand.IsHidden = true;
|
||||
|
||||
listAadAppsCommand.SetHandler(MsIdentity.HandleListApps, provisioningToolBinder);
|
||||
listServicePrincipalsCommand.SetHandler(MsIdentity.HandleListServicePrincipals, provisioningToolBinder);
|
||||
listTenantsCommand.SetHandler(MsIdentity.HandleListTenants, provisioningToolBinder);
|
||||
registerApplicationCommand.SetHandler(MsIdentity.HandleRegisterApplication, provisioningToolBinder);
|
||||
unregisterApplicationCommand.SetHandler(MsIdentity.HandleUnregisterApplication, provisioningToolBinder);
|
||||
updateAppRegistrationCommand.SetHandler(MsIdentity.HandleUpdateApplication, provisioningToolBinder);
|
||||
updateProjectCommand.SetHandler(MsIdentity.HandleUpdateProject, provisioningToolBinder);
|
||||
createClientSecretCommand.SetHandler(MsIdentity.HandleClientSecrets, provisioningToolBinder);
|
||||
createAppRegistration.SetHandler(MsIdentity.HandleCreateAppRegistration, provisioningToolBinder);
|
||||
|
||||
rootCommand.AddCommand(listAadAppsCommand);
|
||||
rootCommand.AddCommand(listServicePrincipalsCommand);
|
||||
rootCommand.AddCommand(listTenantsCommand);
|
||||
rootCommand.AddCommand(registerApplicationCommand);
|
||||
rootCommand.AddCommand(unregisterApplicationCommand);
|
||||
rootCommand.AddCommand(createAppRegistration);
|
||||
rootCommand.AddCommand(updateAppRegistrationCommand);
|
||||
rootCommand.AddCommand(updateProjectCommand);
|
||||
rootCommand.AddCommand(createClientSecretCommand);
|
||||
|
||||
rootCommand.Description = "dotnet scaffold [command] [-p|--project] [-n|--nuget-package-dir] [-c|--configuration] [-tfm|--target-framework] [-b|--build-base-path] [--no-build] ";
|
||||
if (args.Length == 0)
|
||||
{
|
||||
args = new string[] { "-h" };
|
||||
}
|
||||
|
||||
var commandLineBuilder = new CommandLineBuilder(rootCommand);
|
||||
commandLineBuilder.UseDefaults();
|
||||
var parser = commandLineBuilder.Build();
|
||||
int parseExitCode = parser.Invoke(args);
|
||||
if (parseExitCode != 0)
|
||||
{
|
||||
return parseExitCode;
|
||||
}
|
||||
|
||||
ParseResult parseResult = parser.Parse(args);
|
||||
string parsedCommandName = parseResult.CommandResult.Command.Name;
|
||||
switch (parsedCommandName)
|
||||
{
|
||||
case AREA_COMMAND:
|
||||
case CONTROLLER_COMMAND:
|
||||
case IDENTITY_COMMAND:
|
||||
case RAZORPAGE_COMMAND:
|
||||
case VIEW_COMMAND:
|
||||
if (parseResult.CommandResult.Children.Count == 1 &&
|
||||
string.Equals(parseResult.CommandResult.Children[0].Symbol?.Name, "help", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// The help option for the commands are handled by System.Commandline.
|
||||
return 0;
|
||||
}
|
||||
args[0] = args[0].Replace("--", "");
|
||||
return VisualStudio.Web.CodeGeneration.Tools.Program.Main(args);
|
||||
default:
|
||||
// The command is not handled by 'dotnet scaffold'.
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static Option ProjectOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-p", "--project" },
|
||||
description: "Specifies the path of the project file to run (folder name or full path). If not specified, it defaults to the current directory.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option NuGetPackageOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-n", "--nuget-package-dir" },
|
||||
description: "Specifies the NuGet package directory.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option ConfigurationOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-c", "--configuration" },
|
||||
defaultValueFactory: () => "Debug",
|
||||
description: "Defines the build configuration. The default value is Debug.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option TargetFrameworkOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-tfm", "--target-framework" },
|
||||
description: "Target Framework to use. For example, net46.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option BuildBasePathOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-b", "--build-base-path" },
|
||||
description: "The build base path.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option NoBuildOption() =>
|
||||
new Option<bool>(
|
||||
aliases: new[] { "--no-build" },
|
||||
description: "Doesn't build the project before running. It also implicitly sets the --no-restore flag.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Command ScaffoldCommand() =>
|
||||
new Command(
|
||||
name: SCAFFOLD_COMMAND,
|
||||
description: "Scaffold files to a .NET Application")
|
||||
{
|
||||
// Arguments & Options
|
||||
ProjectOption(), NuGetPackageOption(), ConfigurationOption(), TargetFrameworkOption(), BuildBasePathOption(), NoBuildOption()
|
||||
};
|
||||
|
||||
// AREA SCAFFOLDERS
|
||||
private static Argument AreaNameArgument() =>
|
||||
new Argument<string>(
|
||||
name: "AreaNameToGenerate",
|
||||
description: "This tool is intended for ASP.NET Core web projects with controllers and views. It's not intended for Razor Pages apps.")
|
||||
{
|
||||
Arity = ArgumentArity.ExactlyOne
|
||||
};
|
||||
|
||||
private static Command ScaffoldAreaCommand() =>
|
||||
new Command(
|
||||
name: AREA_COMMAND,
|
||||
description: "Scaffolds an Area")
|
||||
{
|
||||
// Arguments & Options
|
||||
AreaNameArgument()
|
||||
};
|
||||
|
||||
private static Option ControllerNameOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-name", "--controllerName" },
|
||||
description: "Name of the controller.")
|
||||
{
|
||||
IsRequired = true
|
||||
};
|
||||
|
||||
private static Option AsyncActionsOption() =>
|
||||
new Option<bool>(
|
||||
aliases: new[] { "-async", "--useAsyncActions" },
|
||||
description: "Generate async controller actions.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
private static Option GenerateNoViewOption() =>
|
||||
new Option<bool>(
|
||||
aliases: new[] { "-nv", "--noViews" },
|
||||
description: "Generate no views.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
private static Option RestWithNoViewOption() =>
|
||||
new Option<bool>(
|
||||
aliases: new[] { "-api", "--restWithNoViews" },
|
||||
description: "Generate a Controller with REST style API. noViews is assumed and any view related options are ignored.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option ReadWriteActionOption() =>
|
||||
new Option<bool>(
|
||||
aliases: new[] { "-actions", "--readWriteActions" },
|
||||
description: "Generate controller with read/write actions without a model.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option ModelClassOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-m", "--model" },
|
||||
description: "Model class to use.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option DataContextOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-dc", "--dataContext" },
|
||||
description: "The DbContext class to use.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option BootStrapVersionOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-b", "--bootstrapVersion" },
|
||||
description: "Specifies the bootstrap version. Valid values are 3 or 4. Default is 4. If needed and not present, a wwwroot directory is created that includes the bootstrap files of the specified version.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option ReferenceScriptLibrariesOption() =>
|
||||
new Option<bool>(
|
||||
aliases: new[] { "-scripts", "--referenceScriptLibraries" },
|
||||
description: "Reference script libraries in the generated views. Adds _ValidationScriptsPartial to Edit and Create pages.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option CustomLayoutOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-l", "--layout" },
|
||||
description: "Custom Layout page to use.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option UseDefaultLayoutOption() =>
|
||||
new Option<bool>(
|
||||
aliases: new[] { "-udl", "--useDefaultLayout" },
|
||||
description: "Use the default layout for the views.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option OverwriteFilesOption() =>
|
||||
new Option<bool>(
|
||||
aliases: new[] { "-f", "--force" },
|
||||
description: "Overwrite existing files.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option RelativeFolderPathOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-outDir", "--relativeFolderPath" },
|
||||
description: "The relative output folder path from project where the file are generated. If not specified, files are generated in the project folder.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option ControllerNamespaceOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-namespace", "--controllerNamespace" },
|
||||
description: "Specify the name of the namespace to use for the generated controller.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option UseSQLliteOption() =>
|
||||
new Option<bool>(
|
||||
aliases: new[] { "-sqlite", "--useSqlite" },
|
||||
description: "Flag to specify if DbContext should use SQLite instead of SQL Server.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Command ScaffoldControllerCommand() =>
|
||||
new Command(
|
||||
name: CONTROLLER_COMMAND,
|
||||
description: "Scaffolds a Controller")
|
||||
{
|
||||
// Arguments & Options
|
||||
ControllerNameOption(), AsyncActionsOption(), GenerateNoViewOption(), RestWithNoViewOption(), ReadWriteActionOption(),
|
||||
ModelClassOption(), DataContextOption(), BootStrapVersionOption(), ReferenceScriptLibrariesOption(), CustomLayoutOption(), UseDefaultLayoutOption(), OverwriteFilesOption(), RelativeFolderPathOption(),
|
||||
ControllerNamespaceOption(), UseSQLliteOption()
|
||||
};
|
||||
|
||||
private static Option RazorpageNamespaceNameOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-namespace", "--namespaceName" },
|
||||
description: "The name of the namespace to use for the generated PageModel.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option PartialViewOption() =>
|
||||
new Option<bool>(
|
||||
aliases: new[] { "-partial", "--partialView" },
|
||||
description: "Generate a partial view. Layout options -l and -udl are ignored if this is specified.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option NoPageModelOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-npm", "--noPageModel" },
|
||||
description: "Switch to not generate a PageModel class for Empty template.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Argument RazorPageNameArgument() =>
|
||||
new Argument<string>(
|
||||
name: "RazorpageNameToGenerate",
|
||||
description: "The name of the razor page to generate.")
|
||||
{
|
||||
Arity = ArgumentArity.ZeroOrOne
|
||||
};
|
||||
|
||||
private static Argument TemplateNameArgument() =>
|
||||
new Argument<string>(
|
||||
name: "TemplateName",
|
||||
description: "Razor Pages or Views can be individually scaffolded by specifying the name of the new page/view and the template to use. The supported templates are: Empty|Create|Edit|Delete|Details|List")
|
||||
{
|
||||
Arity = ArgumentArity.ZeroOrOne
|
||||
};
|
||||
|
||||
private static Command ScaffoldRazorPageCommand() =>
|
||||
new Command(
|
||||
name: RAZORPAGE_COMMAND,
|
||||
description: "Scaffolds Razor pages")
|
||||
{
|
||||
// Arguments
|
||||
RazorPageNameArgument(), TemplateNameArgument(),
|
||||
|
||||
// Options
|
||||
RazorpageNamespaceNameOption(), PartialViewOption(), NoPageModelOption(),
|
||||
ModelClassOption(), DataContextOption(), BootStrapVersionOption(), ReferenceScriptLibrariesOption(), CustomLayoutOption(), UseDefaultLayoutOption(), OverwriteFilesOption(), RelativeFolderPathOption(),
|
||||
UseSQLliteOption()
|
||||
};
|
||||
|
||||
private static Argument ViewNameArgument() =>
|
||||
new Argument<string>(
|
||||
name: "ViewNameToGenerate",
|
||||
description: "The name of the View to generate.")
|
||||
{
|
||||
Arity = ArgumentArity.ZeroOrOne
|
||||
};
|
||||
|
||||
private static Command ScaffoldViewCommand() =>
|
||||
new Command(
|
||||
name: VIEW_COMMAND,
|
||||
description: "Scaffolds a View")
|
||||
{
|
||||
// Arguments
|
||||
ViewNameArgument(),TemplateNameArgument(),
|
||||
|
||||
// Options
|
||||
ModelClassOption(), DataContextOption(), BootStrapVersionOption(), ReferenceScriptLibrariesOption(), CustomLayoutOption(), UseDefaultLayoutOption(), OverwriteFilesOption(), RelativeFolderPathOption(),
|
||||
ControllerNamespaceOption(), UseSQLliteOption(), PartialViewOption()
|
||||
};
|
||||
|
||||
private static Option DBContextOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-dc", "--dbContext" },
|
||||
description: "Name of the DbContext to use, or generate (if it does not exist).")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option FilesListOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-fi", "--files" },
|
||||
description: "List of semicolon separated files to scaffold. Use the --listFiles option to see the available options.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option ListFilesOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-lf", "--listFiles" },
|
||||
description: "Lists the files that can be scaffolded by using the '--files' option.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option UserClassOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-u", "--userClass" },
|
||||
description: "Name of the User class to generate.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option UseDefaultUIOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-udui", "--useDefaultUI" },
|
||||
description: "Use this option to setup identity and to use Default UI.")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Option GenerateLayoutOption() =>
|
||||
new Option<string>(
|
||||
aliases: new[] { "-gl", "--generateLayout" },
|
||||
description: "Use this option to generate a new _Layout.cshtml")
|
||||
{
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
private static Command ScaffoldIdentityCommand() =>
|
||||
new Command(
|
||||
name: IDENTITY_COMMAND,
|
||||
description: "Scaffolds Identity")
|
||||
{
|
||||
// Options
|
||||
DBContextOption(), FilesListOption(), ListFilesOption(), UserClassOption(), UseSQLliteOption(), OverwriteFilesOption(), UseDefaultUIOption(), CustomLayoutOption(),
|
||||
GenerateLayoutOption(), BootStrapVersionOption()
|
||||
};
|
||||
}
|
||||
}
|
||||
var builder = new ScaffoldCommandAppBuilder(args);
|
||||
var app = builder.Build();
|
||||
await app.RunAsync();
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Web.CodeGeneration.Tools.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("E2E_Test.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
|
@ -0,0 +1,104 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using Microsoft.DotNet.Scaffolding.Helpers.Services;
|
||||
using Spectre.Console;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.Services
|
||||
{
|
||||
internal class AnsiConsoleLogger : ILogger
|
||||
{
|
||||
private readonly bool _jsonOutput;
|
||||
private readonly bool _silent;
|
||||
private static bool isTrace = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("codegen_trace"));
|
||||
public bool IsTracing => isTrace;
|
||||
|
||||
private string CommandName { get; }
|
||||
|
||||
public AnsiConsoleLogger(string? commandName = null, bool jsonOutput = false, bool silent = false)
|
||||
{
|
||||
CommandName = commandName ?? string.Empty;
|
||||
_jsonOutput = jsonOutput;
|
||||
_silent = silent;
|
||||
AnsiConsole.Console.Profile.Encoding = Encoding.UTF8;
|
||||
}
|
||||
|
||||
public void LogMessage(string? message, LogMessageType level, bool removeNewLine = false)
|
||||
{
|
||||
//if json output is enabled, don't write to console at all.
|
||||
if (!_silent && !_jsonOutput && !string.IsNullOrEmpty(message))
|
||||
{
|
||||
switch (level)
|
||||
{
|
||||
case LogMessageType.Error:
|
||||
if (removeNewLine)
|
||||
{
|
||||
AnsiConsole.Console.Markup($"[red]{message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.Console.MarkupLine($"[red]{message}");
|
||||
}
|
||||
break;
|
||||
case LogMessageType.Information:
|
||||
if (removeNewLine)
|
||||
{
|
||||
AnsiConsole.Console.Write(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.Console.WriteLine(message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LogJsonMessage(string? state = null, object? content = null, string? output = null)
|
||||
{
|
||||
if (!_silent)
|
||||
{
|
||||
if (_jsonOutput)
|
||||
{
|
||||
var jsonMessage = new JsonResponse(CommandName, state, content, output);
|
||||
AnsiConsole.WriteLine(jsonMessage.ToJsonString());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (state == State.Fail)
|
||||
{
|
||||
LogMessage(output, LogMessageType.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMessage(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LogMessage(string? message, bool removeNewLine = false)
|
||||
{
|
||||
if (!_silent && !_jsonOutput)
|
||||
{
|
||||
LogMessage(message, LogMessageType.Information, removeNewLine);
|
||||
}
|
||||
}
|
||||
|
||||
public void LogFailureAndExit(string failureMessage)
|
||||
{
|
||||
if (_jsonOutput)
|
||||
{
|
||||
LogJsonMessage(State.Fail, output: failureMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMessage(failureMessage, LogMessageType.Error);
|
||||
}
|
||||
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Spectre.Console.Flow;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.Services;
|
||||
|
||||
internal class FlowProvider : IFlowProvider
|
||||
{
|
||||
public FlowProvider()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFlow? CurrentFlow { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFlow GetFlow(IEnumerable<IFlowStep> steps, Dictionary<string, object> properties, bool nonInteractive, bool showSelectedOptions = true)
|
||||
{
|
||||
var flow = new FlowRunner(steps, properties, nonInteractive)
|
||||
.Breadcrumbs()
|
||||
.SelectedOptions(showSelectedOptions);
|
||||
|
||||
CurrentFlow = flow;
|
||||
|
||||
return flow;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Spectre.Console.Flow;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.Scaffold.Services;
|
||||
/// <summary>
|
||||
/// A factory that creates a new flow.
|
||||
/// </summary>
|
||||
public interface IFlowProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Since we are in CLI, only one flow is possible at the given moment. This property contains current flow object.
|
||||
/// </summary>
|
||||
IFlow? CurrentFlow { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the FlowRunner with given steps and properties.
|
||||
/// </summary>
|
||||
IFlow GetFlow(IEnumerable<IFlowStep> steps, Dictionary<string, object> properties, bool nonInteractive, bool showSelectedOptions = true);
|
||||
}
|
|
@ -1,17 +1,30 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Description>Scaffolding tool for .NET projects. Contains the dotnet-scaffold command used for generating areas, views, controllers, identity pages etc. </Description>
|
||||
<Description>Scaffolding tool for .NET projects. </Description>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>exe</OutputType>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackAsTool>true</PackAsTool>
|
||||
<PackageTags>dotnet;scaffold</PackageTags>
|
||||
<PackageId>Microsoft.dotnet-scaffold</PackageId>
|
||||
<RootNamespace>Microsoft.DotNet.Tools.Scaffold</RootNamespace>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)eng\Versions.Scaffold.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(RepoRoot)\tools\dotnet-aspnet-codegenerator\dotnet-aspnet-codegenerator.csproj" />
|
||||
<ProjectReference Include="$(RepoRoot)\tools\dotnet-msidentity\dotnet-msidentity.csproj" />
|
||||
<PackageReference Include="System.Commandline" Version="$(SystemCommandLineVersion)" />
|
||||
<PackageReference Include="Spectre.Console.Flow" Version="$(SpectreConsoleFlowVersion)" />
|
||||
<PackageReference Include="Spectre.Console" Version="$(SpectreConsoleVersion)" />
|
||||
<PackageReference Include="Spectre.Console.Cli" Version="$(SpectreConsoleVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(RepoRoot)src\Shared\Microsoft.DotNet.Scaffolding.Helpers\Microsoft.DotNet.Scaffolding.Helpers.csproj" />
|
||||
<ProjectReference Include="$(RepoRoot)src\Shared\Microsoft.DotNet.Scaffolding.ComponentModel\Microsoft.DotNet.Scaffolding.ComponentModel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
Загрузка…
Ссылка в новой задаче