diff --git a/docs/en/CLI.md b/docs/en/CLI.md index f2e7df219b..829b9480c7 100644 --- a/docs/en/CLI.md +++ b/docs/en/CLI.md @@ -42,6 +42,7 @@ Here, the list of all available commands before explaining their details: * **`login`**: Authenticates on your computer with your [abp.io](https://abp.io/) username and password. * **`logout`**: Logouts from your computer if you've authenticated before. * **`bundle`**: Generates script and style references for an ABP Blazor project. +* **`install-libs`**: Install NPM Packages for MVC / Razor Pages and Blazor Server UI types. ### help @@ -433,3 +434,18 @@ abp bundle [options] `bundle` command reads the `appsettings.json` file inside the Blazor project for bundling options. For more details about managing style and script references in Blazor apps, see [Managing Global Scripts & Styles](UI/Blazor/Global-Scripts-Styles.md) +### install-libs + +This command install NPM Packages for MVC / Razor Pages and Blazor Server UI types. Its **executing directory** or passed ```--working-directory``` parameter's directory must contain a project file(*.csproj). + +`install-libs` command reads the `abp.resourcemapping.js` file to manage package. For more details see [Client Side Package Management](UI/AspNetCore/Client-Side-Package-Management.md). + +Usage: + +````bash +abp install-libs [options] +```` + +#### Options + +* ```--working-directory``` or ```-wd```: Specifies the working directory. This option is useful when executing directory doesn't contain a project file. \ No newline at end of file diff --git a/docs/zh-Hans/CLI.md b/docs/zh-Hans/CLI.md index 262f546bf2..4325ee1f07 100644 --- a/docs/zh-Hans/CLI.md +++ b/docs/zh-Hans/CLI.md @@ -39,6 +39,7 @@ dotnet tool update -g Volo.Abp.Cli * **`translate`**: 当源代码控制存储库中有多个JSON[本地化](Localization.md文件时,可简化翻译本地化文件的过程. * **`login`**: 使用你在[abp.io](https://abp.io/)的用户名和密码在你的计算机上认证. * **`logout`**: 在你的计算机注销认证. +* **`install-libs`**: 为 MVC / Razor Pages 和 Blazor Server UI 类型安装NPM包. ### help @@ -339,3 +340,19 @@ abp login -p -o # You can enter both your ``` abp logout ``` + +### install-libs + +为 MVC / Razor Pages 和 Blazor Server UI 类型安装NPM包, 它的 **执行目录** 或者传递的 ```--working-directory``` 目录必须包含一个项目文件(*.csproj). + +`install-libs` 命令读取 `abp.resourcemapping.js` 来管理包. 参阅[客户端包管理](UI/AspNetCore/Client-Side-Package-Management.md)了解更多细节. + +用法: + +````bash +abp install-libs [options] +```` + +#### Options + +* ```--working-directory``` 或 ```-wd```: 指定工作目录, 当执行目录不包含项目文件时会很有用. \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj index 1b7603194a..4f28bad500 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj +++ b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj @@ -13,6 +13,7 @@ + diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs index b3de9d71ff..08da2e3d00 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs @@ -2,9 +2,11 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Cli.Commands; using Volo.Abp.Cli.Http; +using Volo.Abp.Cli.LIbs; using Volo.Abp.Domain; using Volo.Abp.IdentityModel; using Volo.Abp.Json; +using Volo.Abp.Json.SystemTextJson; using Volo.Abp.Minify; using Volo.Abp.Modularity; @@ -25,6 +27,11 @@ namespace Volo.Abp.Cli Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + Configure(options => + { + options.UnsupportedTypes.Add(typeof(ResourceMapping)); + }); + Configure(options => { //TODO: Define constants like done for GenerateProxyCommand.Name. @@ -49,6 +56,7 @@ namespace Volo.Abp.Cli options.Commands["build"] = typeof(BuildCommand); options.Commands["bundle"] = typeof(BundleCommand); options.Commands["create-migration-and-run-migrator"] = typeof(CreateMigrationAndRunMigratorCommand); + options.Commands["install-libs"] = typeof(InstallLibsCommand); }); } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/InstallLibsCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/InstallLibsCommand.cs new file mode 100644 index 0000000000..36db14856f --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/InstallLibsCommand.cs @@ -0,0 +1,78 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.Cli.Args; +using Volo.Abp.Cli.LIbs; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Commands +{ + public class InstallLibsCommand : IConsoleCommand, ITransientDependency + { + public ILogger Logger { get; set; } + + protected IInstallLibsService InstallLibsService { get; } + + public InstallLibsCommand(IInstallLibsService installLibsService) + { + InstallLibsService = installLibsService; + Logger = NullLogger.Instance; + } + + public async Task ExecuteAsync(CommandLineArgs commandLineArgs) + { + var workingDirectoryArg = commandLineArgs.Options.GetOrNull( + Options.WorkingDirectory.Short, + Options.WorkingDirectory.Long + ); + + var workingDirectory = workingDirectoryArg ?? Directory.GetCurrentDirectory(); + + if (!Directory.Exists(workingDirectory)) + { + throw new CliUsageException( + "Specified directory does not exist." + + Environment.NewLine + Environment.NewLine + + GetUsageInfo() + ); + } + + await InstallLibsService.InstallLibs(workingDirectory); + } + + public string GetUsageInfo() + { + var sb = new StringBuilder(); + + sb.AppendLine(""); + sb.AppendLine("Usage:"); + sb.AppendLine(""); + sb.AppendLine(" abp install-libs"); + sb.AppendLine(""); + sb.AppendLine("Options:"); + sb.AppendLine(""); + sb.AppendLine("-wd|--working-directory (default: empty)"); + sb.AppendLine(""); + sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI"); + + return sb.ToString(); + } + + public string GetShortDescription() + { + return "Install NPM Packages for MVC / Razor Pages and Blazor Server UI types."; + } + + public static class Options + { + public static class WorkingDirectory + { + public const string Short = "wd"; + public const string Long = "working-directory"; + } + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/IInstallLibsService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/IInstallLibsService.cs new file mode 100644 index 0000000000..af96df6e22 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/IInstallLibsService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Cli.LIbs +{ + public interface IInstallLibsService + { + Task InstallLibs(string directory); + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs new file mode 100644 index 0000000000..7416859486 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.FileSystemGlobbing; +using Microsoft.Extensions.FileSystemGlobbing.Abstractions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using NuGet.Versioning; +using Volo.Abp.Cli.Utils; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Json; + +namespace Volo.Abp.Cli.LIbs +{ + public class InstallLibsService : IInstallLibsService, ITransientDependency + { + public ILogger Logger { get; set; } + + private readonly IJsonSerializer _jsonSerializer; + + public InstallLibsService(IJsonSerializer jsonSerializer) + { + _jsonSerializer = jsonSerializer; + Logger = NullLogger.Instance; + } + + public async Task InstallLibs(string directory) + { + var projectFiles = Directory.GetFiles(directory, "*.csproj"); + if (!projectFiles.Any()) + { + Logger.LogError("No project file found in the directory."); + return; + } + + if (!await CanInstallLibs(directory)) + { + Logger.LogWarning( + "abp install-libs command is available for MVC, Razor Page, and Blazor-Server UI types"); + return; + } + + if (!IsNpmInstalled()) + { + Logger.LogWarning("NPM is not installed, visit https://nodejs.org/en/download/ and install NPM"); + return; + } + + if (IsYarnAvailable()) + { + RunYarn(directory); + } + else + { + RunNpmInstall(directory); + } + + await CleanAndCopyResources(directory); + } + + private async Task CleanAndCopyResources(string fileDirectory) + { + var mappingFiles = Directory.GetFiles(fileDirectory, "abp.resourcemapping.js", SearchOption.AllDirectories); + var resourceMapping = new ResourceMapping + { + Clean = new List {"./wwwroot/libs"} + }; + + foreach (var mappingFile in mappingFiles) + { + using (var reader = File.OpenText(mappingFile)) + { + var mappingFileContent = await reader.ReadToEndAsync(); + + var mapping = _jsonSerializer.Deserialize(mappingFileContent + .Replace("module.exports", string.Empty) + .Replace("=", string.Empty).Trim().TrimEnd(';')); + + mapping.ReplaceAliases(); + + mapping.Clean.ForEach(c => resourceMapping.Clean.AddIfNotContains(c)); + mapping.Aliases.ToList().ForEach(x => + { + resourceMapping.Aliases.AddIfNotContains(new KeyValuePair(x.Key, x.Value)); + }); + mapping.Mappings.ToList().ForEach(x => + { + resourceMapping.Mappings.AddIfNotContains(new KeyValuePair(x.Key, x.Value)); + }); + } + } + + CleanDirsAndFiles(fileDirectory, resourceMapping); + CopyResourcesToLibs(fileDirectory, resourceMapping); + } + + private void CopyResourcesToLibs(string fileDirectory, ResourceMapping resourceMapping) + { + foreach (var mapping in resourceMapping.Mappings) + { + var destPath = Path.Combine(fileDirectory, mapping.Value); + + var files = FindFiles(fileDirectory, mapping.Key); + + foreach (var file in files) + { + var destFilePath = Path.Combine(destPath, Path.GetFileName(file)); + if (File.Exists(destFilePath)) + { + continue; + } + + Directory.CreateDirectory(Path.GetFullPath(destPath)); + File.Copy(file, destFilePath); + + } + } + } + + private async Task CanInstallLibs(string fileDirectory) + { + var projectFiles = Directory.GetFiles(fileDirectory, "*.csproj"); + + using (var reader = File.OpenText(projectFiles[0])) + { + var projectFileContent = await reader.ReadToEndAsync(); + + return projectFileContent.Contains("Microsoft.NET.Sdk.Web"); + } + } + + private void CleanDirsAndFiles(string directory, ResourceMapping resourceMapping) + { + var files = FindFiles(directory, resourceMapping.Clean.ToArray()); + + foreach (var file in files) + { + if (File.Exists(file)) + { + File.Delete(file); + } + } + + foreach (var directoryInfo in Directory.GetDirectories(Path.Combine(directory, resourceMapping.Clean.First()),"*", SearchOption.AllDirectories).Reverse()) + { + if (!Directory.EnumerateFileSystemEntries(directoryInfo).Any()) + { + Directory.Delete(directoryInfo); + } + } + } + + private string[] FindFiles(string directory, params string[] patterns) + { + var matcher = new Matcher(); + + foreach (var pattern in patterns) + { + if (pattern.StartsWith("!")) + { + matcher.AddExclude(NormalizeGlob(pattern).TrimStart('!')); + } + else + { + matcher.AddInclude(NormalizeGlob(pattern)); + } + } + + var result = matcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(directory))); + + return result.Files.Select(x => Path.Combine(directory, x.Path)).ToArray(); + } + + private string NormalizeGlob(string pattern) + { + pattern = pattern.Replace("//", "/"); + + if (!Path.HasExtension(pattern) && !pattern.EndsWith("*")) + { + return pattern.EnsureEndsWith('/') + "**"; + } + + return pattern; + } + + private void RunNpmInstall(string directory) + { + Logger.LogInformation($"Running npm install on {directory}"); + CmdHelper.RunCmd($"cd {directory} && npm install"); + } + + private void RunYarn(string directory) + { + Logger.LogInformation($"Running Yarn on {directory}"); + CmdHelper.RunCmd($"cd {directory} && yarn"); + } + + private bool IsNpmInstalled() + { + var output = CmdHelper.RunCmdAndGetOutput("npm -v").Trim(); + return SemanticVersion.TryParse(output, out _); + } + + private bool IsYarnAvailable() + { + var output = CmdHelper.RunCmdAndGetOutput("npm list yarn -g").Trim(); + if (output.Contains("empty")) + { + return false; + } + + if (!SemanticVersion.TryParse(output.Substring(output.IndexOf('@') + 1), out var version)) + { + return false; + } + + return version > SemanticVersion.Parse("1.20.0"); + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/ResourceMapping.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/ResourceMapping.cs new file mode 100644 index 0000000000..ed46b833ad --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/ResourceMapping.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Volo.Abp.Cli.LIbs +{ + public class ResourceMapping + { + public Dictionary Aliases { get; set; } + + public List Clean { get; set; } + + public Dictionary Mappings { get; set; } + + public ResourceMapping() + { + Aliases = new Dictionary + { + {"@node_modules", "./node_modules"}, + {"@libs", "./wwwroot/libs"}, + }; + Clean = new List(); + Mappings = new Dictionary(); + } + + public void ReplaceAliases() + { + for (var i = 0; i < Mappings.Count; i++) + { + var mapping = Mappings.ElementAt(i); + Mappings.Remove(mapping.Key); + + var key = mapping.Key; + var value = mapping.Value; + + foreach (var alias in Aliases) + { + key = key.Replace(alias.Key, alias.Value); + value = value.Replace(alias.Key, alias.Value); + } + + Mappings[key] = value; + } + + for (var i = 0; i < Clean.Count; i++) + { + foreach (var alias in Aliases) + { + Clean[i] = Clean[i].Replace(alias.Key, alias.Value); + } + } + } + } +}