mirror of https://github.com/abpframework/abp.git
14 changed files with 435 additions and 34 deletions
@ -0,0 +1,21 @@ |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling |
|||
{ |
|||
public class BundleOptions |
|||
{ |
|||
[NotNull] |
|||
public string Directory { get; set; } |
|||
|
|||
[NotNull] |
|||
public string BundleName { get; set; } |
|||
|
|||
[NotNull] |
|||
public string FrameworkVersion { get; set; } |
|||
|
|||
[NotNull] |
|||
public string ProjectFileName { get; set; } |
|||
|
|||
public bool Minify { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,116 @@ |
|||
using Microsoft.Extensions.Logging; |
|||
using System; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Xml; |
|||
using Volo.Abp.Bundling; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Minify; |
|||
using Volo.Abp.Minify.NUglify; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling |
|||
{ |
|||
public abstract class BundlerBase : IBundler, ITransientDependency |
|||
{ |
|||
private static string[] _minFileSuffixes = { "min", "prod" }; |
|||
|
|||
protected IMinifier Minifier { get; } |
|||
protected ILogger<BundlerBase> Logger { get; set; } |
|||
public abstract string FileExtension { get; } |
|||
public abstract string GenerateDefinition(string bundleFilePath); |
|||
|
|||
protected BundlerBase(IMinifier minifier) |
|||
{ |
|||
Minifier = minifier; |
|||
} |
|||
|
|||
public string Bundle(BundleOptions options, BundleContext context) |
|||
{ |
|||
var bundleFilePath = Path.Combine(PathHelper.GetWwwRootPath(options.Directory), $"{options.BundleName}{FileExtension}"); |
|||
var bundledContent = BundleFiles(options, context); |
|||
File.WriteAllText(bundleFilePath, bundledContent); |
|||
|
|||
return GenerateDefinition(bundleFilePath); |
|||
} |
|||
|
|||
private bool IsMinFile(string fileName) |
|||
{ |
|||
foreach (var suffix in _minFileSuffixes) |
|||
{ |
|||
if (fileName.EndsWith($".{suffix}.{FileExtension}", StringComparison.InvariantCultureIgnoreCase)) |
|||
{ |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private string BundleFiles(BundleOptions options, BundleContext context) |
|||
{ |
|||
var staticAssetsFilePath = Path.Combine(options.Directory, "bin", "Debug", options.FrameworkVersion, $"{options.ProjectFileName}.StaticWebAssets.xml"); |
|||
if (!File.Exists(staticAssetsFilePath)) |
|||
{ |
|||
throw new BundlingException("Unable to find static web assets file. You need to build the project to generate static web assets file."); |
|||
} |
|||
|
|||
var staticAssetsDefinitions = new XmlDocument(); |
|||
staticAssetsDefinitions.Load(staticAssetsFilePath); |
|||
|
|||
var builder = new StringBuilder(); |
|||
foreach (var definition in context.BundleDefinitions) |
|||
{ |
|||
string content; |
|||
if (definition.Source.StartsWith("_content")) |
|||
{ |
|||
var pathFragments = definition.Source.Split('/').ToList(); |
|||
var basePath = $"{pathFragments[0]}/{pathFragments[1]}"; |
|||
var path = staticAssetsDefinitions.SelectSingleNode($"//ContentRoot[@BasePath='{basePath}']").Attributes["Path"].Value; |
|||
var absolutePath = definition.Source.Replace(basePath, path); |
|||
content = GetFileContent(absolutePath, options.Minify); |
|||
} |
|||
else if (definition.Source.StartsWith("_framework")) |
|||
{ |
|||
var slashIndex = definition.Source.IndexOf('/'); |
|||
var fileName = definition.Source.Substring(slashIndex + 1, definition.Source.Length - slashIndex - 1); |
|||
var filePath = Path.Combine(PathHelper.GetFrameworkFolderPath(options.Directory, options.FrameworkVersion), fileName); |
|||
content = GetFileContent(filePath, false); |
|||
} |
|||
else |
|||
{ |
|||
var filePath = Path.Combine(PathHelper.GetWwwRootPath(options.Directory), definition.Source); |
|||
content = GetFileContent(filePath, options.Minify); |
|||
} |
|||
|
|||
content = ProcessBeforeAddingToTheBundle(definition.Source, Path.Combine(options.Directory, "wwwroot"), content); |
|||
builder.AppendLine(content); |
|||
} |
|||
|
|||
return builder.ToString(); |
|||
} |
|||
|
|||
private string GetFileContent(string filePath, bool minify) |
|||
{ |
|||
var content = File.ReadAllText(filePath); |
|||
if (minify && !IsMinFile(filePath)) |
|||
{ |
|||
try |
|||
{ |
|||
content = Minifier.Minify(content); |
|||
} |
|||
catch (NUglifyException ex) |
|||
{ |
|||
Logger.LogWarning($"Unable to minify the file: {Path.GetFileName(filePath)}. Adding file to the bundle without minification.", ex); |
|||
} |
|||
} |
|||
|
|||
return content; |
|||
} |
|||
|
|||
protected virtual string ProcessBeforeAddingToTheBundle(string referencePath, string bundleDirectory, string fileContent) |
|||
{ |
|||
return fileContent; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
namespace Volo.Abp.Cli.Bundling |
|||
{ |
|||
internal static class BundlingConsts |
|||
{ |
|||
internal const string StylePlaceholderStart = "<!--ABP:Styles-->"; |
|||
internal const string StylePlaceholderEnd = "<!--/ABP:Styles-->"; |
|||
internal const string ScriptPlaceholderStart = "<!--ABP:Scripts-->"; |
|||
internal const string ScriptPlaceholderEnd = "<!--/ABP:Scripts-->"; |
|||
internal const string SupportedWebAssemblyProjectType = "Microsoft.NET.Sdk.BlazorWebAssembly"; |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using Volo.Abp.Bundling; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling |
|||
{ |
|||
public interface IBundler |
|||
{ |
|||
string Bundle(BundleOptions options, BundleContext context); |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System.IO; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling |
|||
{ |
|||
internal static class PathHelper |
|||
{ |
|||
internal static string GetFrameworkFolderPath(string projectDirectory, string frameworkVersion) |
|||
{ |
|||
return Path.Combine(projectDirectory, "bin", "Debug", frameworkVersion, "wwwroot", "_framework"); ; |
|||
} |
|||
|
|||
internal static string GetAssemblyFilePath(string directory, string frameworkVersion, string projectFileName) |
|||
{ |
|||
var outputDirectory = GetFrameworkFolderPath(directory, frameworkVersion); |
|||
return Path.Combine(outputDirectory, projectFileName + ".dll"); |
|||
} |
|||
|
|||
internal static string GetWwwRootPath(string directory) |
|||
{ |
|||
return Path.Combine(directory, "wwwroot"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling.Scripts |
|||
{ |
|||
public interface IScriptBundler : IBundler |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Text; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Minify.Scripts; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling.Scripts |
|||
{ |
|||
public class ScriptBundler : BundlerBase, IScriptBundler, ITransientDependency |
|||
{ |
|||
public override string FileExtension => ".js"; |
|||
|
|||
public ScriptBundler(IJavascriptMinifier minifier) |
|||
: base(minifier) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public override string GenerateDefinition(string bundleFilePath) |
|||
{ |
|||
var lastModifiedTicks = File.GetLastWriteTime(bundleFilePath).Ticks; |
|||
var builder = new StringBuilder(); |
|||
builder.AppendLine($"{BundlingConsts.ScriptPlaceholderStart}"); |
|||
builder.AppendLine($" <script src=\"{Path.GetFileName(bundleFilePath)}?_v ={lastModifiedTicks}\"></script>"); |
|||
builder.Append($" {BundlingConsts.ScriptPlaceholderEnd}"); |
|||
return builder.ToString(); |
|||
} |
|||
|
|||
protected override string ProcessBeforeAddingToTheBundle(string referencePath, string bundleDirectory, string fileContent) |
|||
{ |
|||
return fileContent.EnsureEndsWith(';') + Environment.NewLine; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Text.RegularExpressions; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling.Styles |
|||
{ |
|||
internal static class CssRelativePathAdjuster |
|||
{ |
|||
private static readonly Regex _rxUrl = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); |
|||
|
|||
public static string Adjust( |
|||
string cssFileContents, |
|||
string absoluteInputFilePath, |
|||
string absoluteOutputPath) |
|||
{ |
|||
var matches = _rxUrl.Matches(cssFileContents); |
|||
|
|||
if (matches.Count <= 0) |
|||
{ |
|||
return cssFileContents; |
|||
} |
|||
|
|||
var cssDirectoryPath = Path.GetDirectoryName(absoluteInputFilePath); |
|||
|
|||
foreach (Match match in matches) |
|||
{ |
|||
string quoteDelimiter = match.Groups[1].Value; //url('') vs url("")
|
|||
string relativePathToCss = match.Groups[2].Value; |
|||
|
|||
// Ignore root relative references
|
|||
if (relativePathToCss.StartsWith("/", StringComparison.Ordinal)) |
|||
continue; |
|||
|
|||
//prevent query string from causing error
|
|||
var pathAndQuery = relativePathToCss.Split(new[] { '?' }, 2, StringSplitOptions.RemoveEmptyEntries); |
|||
var pathOnly = pathAndQuery[0]; |
|||
var queryOnly = pathAndQuery.Length == 2 ? pathAndQuery[1] : string.Empty; |
|||
|
|||
string absolutePath = GetAbsolutePath(cssDirectoryPath, pathOnly); |
|||
string serverRelativeUrl = MakeRelative(absoluteOutputPath, absolutePath); |
|||
|
|||
if (!string.IsNullOrEmpty(queryOnly)) |
|||
serverRelativeUrl += "?" + queryOnly; |
|||
|
|||
string replace = string.Format("url({0}{1}{0})", quoteDelimiter, serverRelativeUrl); |
|||
|
|||
cssFileContents = cssFileContents.Replace(match.Groups[0].Value, replace); |
|||
} |
|||
|
|||
return cssFileContents; |
|||
} |
|||
|
|||
private static string GetAbsolutePath(string cssFilePath, string pathOnly) |
|||
{ |
|||
return Path.GetFullPath(Path.Combine(cssFilePath, pathOnly)); |
|||
} |
|||
|
|||
private static readonly string _protocol = "file:///"; |
|||
private static string MakeRelative(string baseFile, string file) |
|||
{ |
|||
if (string.IsNullOrEmpty(file)) |
|||
return file; |
|||
|
|||
Uri baseUri = new Uri(_protocol + baseFile, UriKind.RelativeOrAbsolute); |
|||
Uri fileUri = new Uri(_protocol + file, UriKind.RelativeOrAbsolute); |
|||
|
|||
if (baseUri.IsAbsoluteUri) |
|||
{ |
|||
return Uri.UnescapeDataString(baseUri.MakeRelativeUri(fileUri).ToString()); |
|||
} |
|||
else |
|||
{ |
|||
return baseUri.ToString(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling.Styles |
|||
{ |
|||
public interface IStyleBundler : IBundler |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using System.IO; |
|||
using System.Text; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Minify.Styles; |
|||
|
|||
namespace Volo.Abp.Cli.Bundling.Styles |
|||
{ |
|||
public class StyleBundler : BundlerBase, IStyleBundler, ITransientDependency |
|||
{ |
|||
public override string FileExtension => ".css"; |
|||
|
|||
public StyleBundler(ICssMinifier minifier) |
|||
: base(minifier) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public override string GenerateDefinition(string bundleFilePath) |
|||
{ |
|||
var lastModifiedTicks = File.GetLastWriteTime(bundleFilePath).Ticks; |
|||
var builder = new StringBuilder(); |
|||
builder.AppendLine($"{BundlingConsts.StylePlaceholderStart}"); |
|||
builder.AppendLine($" <link href=\"{Path.GetFileName(bundleFilePath)}?_v ={lastModifiedTicks}\" rel=\"stylesheet\"/>"); |
|||
builder.Append($" {BundlingConsts.StylePlaceholderEnd}"); |
|||
return builder.ToString(); |
|||
} |
|||
|
|||
protected override string ProcessBeforeAddingToTheBundle(string referencePath, string bundleDirectory, string fileContent) |
|||
{ |
|||
return CssRelativePathAdjuster.Adjust( |
|||
fileContent, |
|||
referencePath, |
|||
bundleDirectory |
|||
); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue