Bundling & minification system.

pull/301/head
Halil ibrahim Kalkan 7 years ago
parent 70e3a7f07c
commit 36def34e3d

@ -30,12 +30,13 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Bundling
CreateBundle(bundleName, files);
var bundleFiles = GetBundleFiles(bundleName);
await output.GetChildContentAsync(); //TODO: Suppress child execution!
output.Content.Clear();
AddHtmlTags(context, output, bundleFiles);
}
protected abstract void CreateBundle(string bundleName, List<string> files);
protected abstract List<string> GetBundleFiles(string bundleName);
protected abstract void AddHtmlTags(TagHelperContext context, TagHelperOutput output, List<string> files);
@ -49,7 +50,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Bundling
{
var fileList = new List<string>();
context.Items[AbpBundleFileTagHelperService.ContextFileListKey] = fileList;
await output.GetChildContentAsync();
await output.GetChildContentAsync(); //TODO: Suppress child execution!
return fileList;
}
}

@ -1,6 +0,0 @@
[
{
"outputFile": "wwwroot/libs/abp/aspnetcore.mvc.ui.theme.shared/datatables/datatables.css",
"inputFile": "wwwroot/libs/abp/aspnetcore.mvc.ui.theme.shared/datatables/datatables.scss"
}
]

@ -1,49 +0,0 @@
{
"compilers": {
"less": {
"autoPrefix": "",
"cssComb": "none",
"ieCompat": true,
"strictMath": false,
"strictUnits": false,
"relativeUrls": true,
"rootPath": "",
"sourceMapRoot": "",
"sourceMapBasePath": "",
"sourceMap": false
},
"sass": {
"includePath": "",
"indentType": "space",
"indentWidth": 2,
"outputStyle": "nested",
"Precision": 5,
"relativeUrls": true,
"sourceMapRoot": "",
"sourceMap": false
},
"stylus": {
"sourceMap": false
},
"babel": {
"sourceMap": false
},
"coffeescript": {
"bare": false,
"runtimeMode": "node",
"sourceMap": false
}
},
"minifiers": {
"css": {
"enabled": true,
"termSemicolons": true,
"gzip": false
},
"javascript": {
"enabled": true,
"termSemicolons": true,
"gzip": false
}
}
}

@ -25,13 +25,13 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Bundling
{
context.Files.AddRange(new[]
{
"/libs/abp/aspnetcore.mvc.ui.theme.shared/jquery/jquery-extensions.js",
"/libs/abp/aspnetcore.mvc.ui.theme.shared/jquery-form/jquery-form-extensions.js",
"/libs/abp/aspnetcore.mvc.ui.theme.shared/bootstrap/dom-event-handlers.js",
"/libs/abp/aspnetcore.mvc.ui.theme.shared/bootstrap/modal-manager.js",
"/libs/abp/aspnetcore.mvc.ui.theme.shared/datatables/datatables-extensions.js",
"/libs/abp/aspnetcore.mvc.ui.theme.shared/sweetalert/abp-sweetalert.js",
"/libs/abp/aspnetcore.mvc.ui.theme.shared/toastr/abp-toastr.js"
"/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery/jquery-extensions.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery-form/jquery-form-extensions.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/sweetalert/abp-sweetalert.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/toastr/abp-toastr.js"
});
}
}

@ -21,10 +21,6 @@
<EmbeddedResource Include="Areas\**\*.cshtml" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="Pages\Error\Index.cshtml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="2.1.0" />
</ItemGroup>

@ -1,11 +1,16 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling.Scripts;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling.Styles;
using Volo.Abp.AspNetCore.Mvc.UI.Theming;
using Volo.Abp.DependencyInjection;
using Volo.Abp.IO;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling
{
@ -14,7 +19,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling
{
private readonly BundlingOptions _options;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IBundler _bundler;
private readonly IScriptBundler _scriptBundler;
private readonly IStyleBundler _styleBundler;
private readonly IThemeManager _themeManager;
private readonly IServiceProvider _serviceProvider;
@ -22,15 +28,17 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling
public BundleManager(
IOptions<BundlingOptions> options,
IScriptBundler scriptBundler,
IStyleBundler styleBundler,
IHostingEnvironment hostingEnvironment,
IBundler bundler,
IThemeManager themeManager,
IServiceProvider serviceProvider)
{
_hostingEnvironment = hostingEnvironment;
_bundler = bundler;
_scriptBundler = scriptBundler;
_themeManager = themeManager;
_serviceProvider = serviceProvider;
_styleBundler = styleBundler;
_options = options.Value;
_cache = new ConcurrentDictionary<string, string>();
@ -38,28 +46,63 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling
public List<string> GetStyleBundleFiles(string bundleName)
{
return GetFiles(_options.StyleBundles.Get(bundleName));
return GetBundleFiles(_options.StyleBundles.Get(bundleName), _styleBundler);
}
public List<string> GetScriptBundleFiles(string bundleName)
{
return GetFiles(_options.ScriptBundles.Get(bundleName));
return GetBundleFiles(_options.ScriptBundles.Get(bundleName), _scriptBundler);
}
public void CreateStyleBundle(string bundleName, Action<BundleConfiguration> configureAction)
protected virtual List<string> GetBundleFiles(BundleConfiguration bundleConfiguration, IBundler bundler)
{
//TODO: Caching
//TODO: Concurrency
var files = CreateFileListFromConfiguration(bundleConfiguration);
if (!IsBundlingEnabled())
{
return files;
}
var bundleRelativePath = _options.BundleFolderName.EnsureEndsWith('/') + bundleConfiguration.Name + "." + bundler.FileExtension;
var bundleResult = bundler.Bundle(new BundlerContext(bundleRelativePath, files));
SaveBundleResult(bundleRelativePath, bundleResult);
return new List<string>
{
"/" + bundleRelativePath
};
}
protected virtual void SaveBundleResult(string bundleRelativePath, BundleResult bundleResult)
{
var bundleFilePath = Path.Combine(_hostingEnvironment.WebRootPath, bundleRelativePath);
DirectoryHelper.CreateIfNotExists(Path.GetDirectoryName(bundleFilePath));
File.WriteAllText(bundleFilePath, bundleResult.Content, Encoding.UTF8);
}
public virtual void CreateStyleBundle(string bundleName, Action<BundleConfiguration> configureAction)
{
_options.StyleBundles.GetOrAdd(bundleName, configureAction);
}
public void CreateScriptBundle(string bundleName, Action<BundleConfiguration> configureAction)
public virtual void CreateScriptBundle(string bundleName, Action<BundleConfiguration> configureAction)
{
_options.ScriptBundles.GetOrAdd(bundleName, configureAction);
}
protected virtual List<string> GetFiles(BundleConfiguration bundleConfiguration)
protected virtual bool IsBundlingEnabled()
{
//TODO: Caching, Bundling & Minifying!
return true;
//return !_hostingEnvironment.IsDevelopment();
}
protected virtual List<string> CreateFileListFromConfiguration(BundleConfiguration bundleConfiguration)
{
using (var scope = _serviceProvider.CreateScope())
{
var context = new BundleConfigurationContext(

@ -0,0 +1,12 @@
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling
{
public class BundleResult
{
public string Content { get; }
public BundleResult(string content)
{
Content = content;
}
}
}

@ -1,18 +0,0 @@
using System.Collections.Generic;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling
{
public class Bundler : IBundler, ITransientDependency
{
public Bundler()
{
}
public string CreateBundle(List<string> files)
{
return "";
}
}
}

@ -0,0 +1,53 @@
using System.Text;
using Microsoft.Extensions.FileProviders;
using Volo.Abp.AspNetCore.Mvc.UI.Minification;
using Volo.Abp.AspNetCore.VirtualFileSystem;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling
{
public abstract class BundlerBase : IBundler, ITransientDependency
{
protected IHybridWebRootFileProvider WebRootFileProvider { get; }
protected IMinifier Minifier { get; }
protected BundlerBase(IHybridWebRootFileProvider webRootFileProvider, IMinifier minifier)
{
WebRootFileProvider = webRootFileProvider;
Minifier = minifier;
}
public abstract string FileExtension { get; }
public BundleResult Bundle(IBundlerContext context)
{
var sb = new StringBuilder();
foreach (var file in context.ContentFiles)
{
sb.AppendLine(GetFileContent(context, file));
}
return new BundleResult(
Minifier.Minify(sb.ToString(), context.BundleRelativePath)
);
}
protected virtual string GetFileContent(IBundlerContext context, string file)
{
return GetFileInfo(context, file).ReadAsString();
}
protected virtual IFileInfo GetFileInfo(IBundlerContext context, string file)
{
var fileInfo = WebRootFileProvider.GetFileInfo(file);
if (!fileInfo.Exists)
{
throw new AbpException($"Could not find file '{file}' using {nameof(IHybridWebRootFileProvider)}");
}
return fileInfo;
}
}
}

@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling
{
public class BundlerContext : IBundlerContext
{
public string BundleRelativePath { get; }
public IReadOnlyList<string> ContentFiles { get; }
public BundlerContext(string bundleRelativePath, IReadOnlyList<string> contentFiles)
{
BundleRelativePath = bundleRelativePath;
ContentFiles = contentFiles;
}
}
}

@ -6,6 +6,13 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling
public BundleConfigurationCollection ScriptBundles { get; set; }
//TODO: Add option to enable/disable bundling / minification
/// <summary>
/// Default: "__bundles".
/// </summary>
public string BundleFolderName { get; } = "__bundles";
public BundlingOptions()
{
StyleBundles = new BundleConfigurationCollection();

@ -1,9 +1,9 @@
using System.Collections.Generic;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling
{
public interface IBundler
{
string CreateBundle(List<string> files);
string FileExtension { get; }
BundleResult Bundle(IBundlerContext context);
}
}

@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling
{
public interface IBundlerContext
{
string BundleRelativePath { get; }
IReadOnlyList<string> ContentFiles { get; }
}
}

@ -0,0 +1,7 @@
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.Scripts
{
public interface IScriptBundler : IBundler
{
}
}

@ -0,0 +1,15 @@
using Volo.Abp.AspNetCore.Mvc.UI.Minification.Scripts;
using Volo.Abp.AspNetCore.VirtualFileSystem;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.Scripts
{
public class ScriptBundler : BundlerBase, IScriptBundler
{
public override string FileExtension => "js";
public ScriptBundler(IHybridWebRootFileProvider webRootFileProvider, IJavascriptMinifier minifier)
: base(webRootFileProvider, minifier)
{
}
}
}

@ -0,0 +1,74 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.Styles
{
internal static class CssRelativePath
{
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,7 @@
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.Styles
{
public interface IStyleBundler : IBundler
{
}
}

@ -0,0 +1,27 @@
using System;
using Volo.Abp.AspNetCore.Mvc.UI.Minification.Styles;
using Volo.Abp.AspNetCore.VirtualFileSystem;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.Styles
{
public class StyleBundler : BundlerBase, IStyleBundler
{
public override string FileExtension => "css";
public StyleBundler(IHybridWebRootFileProvider webRootFileProvider, ICssMinifier minifier)
: base(webRootFileProvider, minifier)
{
}
protected override string GetFileContent(IBundlerContext context, string file)
{
var content = base.GetFileContent(context, file);
return CssRelativePath.Adjust(
content,
WebRootFileProvider.GetAbsolutePath(file),
WebRootFileProvider.GetAbsolutePath(context.BundleRelativePath)
);
}
}
}

@ -1,6 +1,6 @@
namespace Volo.Abp.AspNetCore.Mvc.UI.Minification
{
public interface IJavascriptMinifier
public interface IMinifier
{
string Minify(string source, string fileName = null);
}

@ -0,0 +1,13 @@
using NUglify;
using Volo.Abp.AspNetCore.Mvc.UI.Minification.Styles;
namespace Volo.Abp.AspNetCore.Mvc.UI.Minification.NUglify
{
public class NUglifyCssMinifier : NUglifyMinifierBase, ICssMinifier
{
protected override UglifyResult UglifySource(string source, string fileName)
{
return Uglify.Css(source, fileName);
}
}
}

@ -1,26 +1,15 @@
using NUglify;
using NUglify.JavaScript;
using Volo.Abp.AspNetCore.Mvc.UI.Minification.Scripts;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.UI.Minification.NUglify
{
public class NUglifyJavascriptMinifier : IJavascriptMinifier, ITransientDependency
public class NUglifyJavascriptMinifier : NUglifyMinifierBase, IJavascriptMinifier
{
public string Minify(string source, string fileName = null)
protected override UglifyResult UglifySource(string source, string fileName)
{
var result = Uglify.Js(source, fileName);
CheckErrors(result);
return result.Code;
}
private static void CheckErrors(UglifyResult result)
{
if (result.HasErrors)
{
throw new NUglifyException(
"There are some errors on uglifying the given source code!",
result.Errors
);
}
return Uglify.Js(source, fileName);
}
}
}

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUglify;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.UI.Minification.NUglify
{
public abstract class NUglifyMinifierBase : IMinifier, ITransientDependency
{
private static void CheckErrors(UglifyResult result)
{
if (result.HasErrors)
{
throw new NUglifyException(
$"There are some errors on uglifying the given source code!{Environment.NewLine}{result.Errors.Select(err => err.ToString()).JoinAsString(Environment.NewLine)}",
result.Errors
);
}
}
public string Minify(string source, string fileName = null)
{
var result = UglifySource(source, fileName);
CheckErrors(result);
return result.Code;
}
protected abstract UglifyResult UglifySource(string source, string fileName);
}
}

@ -0,0 +1,7 @@
namespace Volo.Abp.AspNetCore.Mvc.UI.Minification.Scripts
{
public interface IJavascriptMinifier : IMinifier
{
}
}

@ -0,0 +1,7 @@
namespace Volo.Abp.AspNetCore.Mvc.UI.Minification.Styles
{
public interface ICssMinifier : IMinifier
{
}
}

@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Builder
IFileProvider fileProvider = new AspNetCoreVirtualFileProvider(
app.ApplicationServices,
"/wwwroot"
"/wwwroot" //TODO: Hard-coded "/wwwroot" is not good!
);
app.UseStaticFiles(

@ -0,0 +1,53 @@
using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using Volo.Abp.DependencyInjection;
using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.AspNetCore.VirtualFileSystem
{
public class HybridWebRootFileProvider : IHybridWebRootFileProvider, ISingletonDependency
{
private readonly IVirtualFileProvider _virtualFileProvider;
private readonly IFileProvider _fileProvider;
private readonly IHostingEnvironment _hostingEnvironment;
public HybridWebRootFileProvider(IVirtualFileProvider virtualFileProvider, IHostingEnvironment hostingEnvironment)
{
_virtualFileProvider = virtualFileProvider;
_hostingEnvironment = hostingEnvironment;
_fileProvider = CreateHybridProvider();
}
public virtual IFileInfo GetFileInfo(string subpath)
{
return _fileProvider.GetFileInfo("/wwwroot" + subpath); //TODO: Hard-coded "/wwwroot" is not good!
}
public virtual IDirectoryContents GetDirectoryContents(string subpath)
{
return _fileProvider.GetDirectoryContents("/wwwroot" + subpath);
}
public virtual IChangeToken Watch(string filter)
{
return _fileProvider.Watch("/wwwroot" + filter);
}
public string GetAbsolutePath(string relativePath)
{
return Path.Combine(_hostingEnvironment.ContentRootPath, "wwwroot", relativePath.RemovePreFix("/"));
}
protected virtual IFileProvider CreateHybridProvider()
{
return new CompositeFileProvider(
_hostingEnvironment.ContentRootFileProvider,
_virtualFileProvider
);
}
}
}

@ -0,0 +1,9 @@
using Microsoft.Extensions.FileProviders;
namespace Volo.Abp.AspNetCore.VirtualFileSystem
{
public interface IHybridWebRootFileProvider : IFileProvider
{
string GetAbsolutePath(string relativePath);
}
}

@ -0,0 +1,22 @@
using System.IO;
namespace Volo.Abp.IO
{
/// <summary>
/// A helper class for Directory operations.
/// </summary>
public static class DirectoryHelper
{
/// <summary>
/// Creates a new directory if it does not exists.
/// </summary>
/// <param name="directory">Directory to create</param>
public static void CreateIfNotExists(string directory)
{
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
}
}
}

@ -0,0 +1,22 @@
using System.IO;
namespace Volo.Abp.IO
{
/// <summary>
/// A helper class for File operations.
/// </summary>
public static class FileHelper
{
/// <summary>
/// Checks and deletes given file if it does exists.
/// </summary>
/// <param name="filePath">Path of the file</param>
public static void DeleteIfExists(string filePath)
{
if (File.Exists(filePath))
{
File.Delete(filePath);
}
}
}
}

@ -19,22 +19,22 @@ namespace Volo.Abp.VirtualFileSystem
_fileProvider = CreateHybridProvider();
}
public IFileInfo GetFileInfo(string subpath)
public virtual IFileInfo GetFileInfo(string subpath)
{
return _fileProvider.GetFileInfo(subpath);
}
public IDirectoryContents GetDirectoryContents(string subpath)
public virtual IDirectoryContents GetDirectoryContents(string subpath)
{
return _fileProvider.GetDirectoryContents(subpath);
}
public IChangeToken Watch(string filter)
public virtual IChangeToken Watch(string filter)
{
return _fileProvider.Watch(filter);
}
private IFileProvider CreateHybridProvider()
protected virtual IFileProvider CreateHybridProvider()
{
IFileProvider fileProvider = new InternalVirtualFileProvider(_options);
@ -54,7 +54,7 @@ namespace Volo.Abp.VirtualFileSystem
return fileProvider;
}
private class InternalVirtualFileProvider : IFileProvider
protected class InternalVirtualFileProvider : IFileProvider
{
private readonly VirtualFileSystemOptions _options;
private readonly Lazy<Dictionary<string, IFileInfo>> _files;

Loading…
Cancel
Save