Files
Umbraco-CMS/src/Umbraco.Web.Common/RuntimeMinification/SmidgeRuntimeMinifier.cs

187 lines
8.9 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Smidge;
using Smidge.Cache;
using Smidge.CompositeFiles;
using Smidge.FileProcessors;
using Smidge.Models;
using Smidge.Nuglify;
using Smidge.Options;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.WebAssets;
using CssFile = Smidge.Models.CssFile;
using JavaScriptFile = Smidge.Models.JavaScriptFile;
namespace Umbraco.Cms.Web.Common.RuntimeMinification
{
public class SmidgeRuntimeMinifier : IRuntimeMinifier
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IConfigManipulator _configManipulator;
private readonly CacheBusterResolver _cacheBusterResolver;
private readonly RuntimeMinificationSettings _runtimeMinificationSettings;
private readonly IBundleManager _bundles;
private readonly SmidgeHelperAccessor _smidge;
// used only for minifying in MinifyAsync not for an actual pipeline
private readonly Lazy<PreProcessPipeline> _jsMinPipeline;
private readonly Lazy<PreProcessPipeline> _cssMinPipeline;
// default pipelines for processing js/css files for the back office
private readonly Lazy<PreProcessPipeline> _jsOptimizedPipeline;
private readonly Lazy<PreProcessPipeline> _jsNonOptimizedPipeline;
private readonly Lazy<PreProcessPipeline> _cssOptimizedPipeline;
private readonly Lazy<PreProcessPipeline> _cssNonOptimizedPipeline;
private ICacheBuster _cacheBuster;
private readonly Type _cacheBusterType;
public SmidgeRuntimeMinifier(
IBundleManager bundles,
SmidgeHelperAccessor smidge,
IHostingEnvironment hostingEnvironment,
IConfigManipulator configManipulator,
IOptions<RuntimeMinificationSettings> runtimeMinificationSettings,
CacheBusterResolver cacheBusterResolver)
{
_bundles = bundles;
_smidge = smidge;
_hostingEnvironment = hostingEnvironment;
_configManipulator = configManipulator;
_cacheBusterResolver = cacheBusterResolver;
_runtimeMinificationSettings = runtimeMinificationSettings.Value;
_jsMinPipeline = new Lazy<PreProcessPipeline>(() => _bundles.PipelineFactory.Create(typeof(JsMinifier)));
_cssMinPipeline = new Lazy<PreProcessPipeline>(() => _bundles.PipelineFactory.Create(typeof(NuglifyCss)));
// replace the default JsMinifier with NuglifyJs and CssMinifier with NuglifyCss in the default pipelines
// for use with our bundles only (not modifying global options)
_jsOptimizedPipeline = new Lazy<PreProcessPipeline>(() => bundles.PipelineFactory.DefaultJs().Replace<JsMinifier, SmidgeNuglifyJs>(_bundles.PipelineFactory));
_jsNonOptimizedPipeline = new Lazy<PreProcessPipeline>(() =>
{
PreProcessPipeline defaultJs = bundles.PipelineFactory.DefaultJs();
// remove minification from this pipeline
defaultJs.Processors.RemoveAll(x => x is JsMinifier);
return defaultJs;
});
_cssOptimizedPipeline = new Lazy<PreProcessPipeline>(() => bundles.PipelineFactory.DefaultCss().Replace<CssMinifier, NuglifyCss>(_bundles.PipelineFactory));
_cssNonOptimizedPipeline = new Lazy<PreProcessPipeline>(() =>
{
PreProcessPipeline defaultCss = bundles.PipelineFactory.DefaultCss();
// remove minification from this pipeline
defaultCss.Processors.RemoveAll(x => x is CssMinifier);
return defaultCss;
});
Type cacheBusterType = _runtimeMinificationSettings.CacheBuster switch
{
RuntimeMinificationCacheBuster.AppDomain => typeof(AppDomainLifetimeCacheBuster),
RuntimeMinificationCacheBuster.Version => typeof(ConfigCacheBuster),
RuntimeMinificationCacheBuster.Timestamp => typeof(TimestampCacheBuster),
_ => throw new NotImplementedException()
};
_cacheBusterType = cacheBusterType;
}
public string CacheBuster => (_cacheBuster ??= _cacheBusterResolver.GetCacheBuster(_cacheBusterType)).GetValue();
// only issue with creating bundles like this is that we don't have full control over the bundle options, though that could
public void CreateCssBundle(string bundleName, BundlingOptions bundleOptions, params string[] filePaths)
{
if (filePaths.Any(f => !f.StartsWith("/") && !f.StartsWith("~/")))
{
throw new InvalidOperationException("All file paths must be absolute");
}
if (_bundles.Exists(bundleName))
{
throw new InvalidOperationException($"The bundle name {bundleName} already exists");
}
PreProcessPipeline pipeline = bundleOptions.OptimizeOutput
? _cssOptimizedPipeline.Value
: _cssNonOptimizedPipeline.Value;
Bundle bundle = _bundles.Create(bundleName, pipeline, WebFileType.Css, filePaths);
bundle.WithEnvironmentOptions(ConfigureBundleEnvironmentOptions(bundleOptions));
}
public async Task<string> RenderCssHereAsync(string bundleName) => (await _smidge.SmidgeHelper.CssHereAsync(bundleName, _hostingEnvironment.IsDebugMode)).ToString();
public void CreateJsBundle(string bundleName, BundlingOptions bundleOptions, params string[] filePaths)
{
if (filePaths.Any(f => !f.StartsWith("/") && !f.StartsWith("~/")))
{
throw new InvalidOperationException("All file paths must be absolute");
}
if (_bundles.Exists(bundleName))
{
throw new InvalidOperationException($"The bundle name {bundleName} already exists");
}
PreProcessPipeline pipeline = bundleOptions.OptimizeOutput
? _jsOptimizedPipeline.Value
: _jsNonOptimizedPipeline.Value;
Bundle bundle = _bundles.Create(bundleName, pipeline, WebFileType.Js, filePaths);
bundle.WithEnvironmentOptions(ConfigureBundleEnvironmentOptions(bundleOptions));
}
private BundleEnvironmentOptions ConfigureBundleEnvironmentOptions(BundlingOptions bundleOptions)
{
var bundleEnvironmentOptions = new BundleEnvironmentOptions();
// auto-invalidate bundle if files change in debug
bundleEnvironmentOptions.DebugOptions.FileWatchOptions.Enabled = true;
// set cache busters
bundleEnvironmentOptions.DebugOptions.SetCacheBusterType(_cacheBusterType);
bundleEnvironmentOptions.ProductionOptions.SetCacheBusterType(_cacheBusterType);
// config if the files should be combined
bundleEnvironmentOptions.ProductionOptions.ProcessAsCompositeFile = bundleOptions.EnabledCompositeFiles;
return bundleEnvironmentOptions;
}
public async Task<string> RenderJsHereAsync(string bundleName) => (await _smidge.SmidgeHelper.JsHereAsync(bundleName, _hostingEnvironment.IsDebugMode)).ToString();
public async Task<IEnumerable<string>> GetJsAssetPathsAsync(string bundleName) => await _smidge.SmidgeHelper.GenerateJsUrlsAsync(bundleName, _hostingEnvironment.IsDebugMode);
public async Task<IEnumerable<string>> GetCssAssetPathsAsync(string bundleName) => await _smidge.SmidgeHelper.GenerateCssUrlsAsync(bundleName, _hostingEnvironment.IsDebugMode);
/// <inheritdoc />
public async Task<string> MinifyAsync(string fileContent, AssetType assetType)
{
switch (assetType)
{
case AssetType.Javascript:
return await _jsMinPipeline.Value
.ProcessAsync(
new FileProcessContext(fileContent, new JavaScriptFile(), BundleContext.CreateEmpty(CacheBuster)));
case AssetType.Css:
return await _cssMinPipeline.Value
.ProcessAsync(new FileProcessContext(fileContent, new CssFile(), BundleContext.CreateEmpty(CacheBuster)));
default:
throw new NotSupportedException("Unexpected AssetType");
}
}
/// <inheritdoc />
/// <remarks>
/// Smidge uses the version number as cache buster (configurable).
/// We therefore can reset, by updating the version number in config
/// </remarks>
public void Reset()
{
var version = DateTime.UtcNow.Ticks.ToString();
_configManipulator.SaveConfigValue(Cms.Core.Constants.Configuration.ConfigRuntimeMinificationVersion, version.ToString());
}
}
}