diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c7be010..a0b943f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -89,7 +89,3 @@ jobs: with: name: smidge-nuget-${{ env.GitVersion_SemVer }} path: ${{ github.workspace }}/_NugetOutput/*.* - - - name: Publish to GitHub Packages - if: ${{ success() && github.event_name == 'pull_request' }} - run: dotnet nuget push "${{ github.workspace }}/_NugetOutput/*.nupkg" --api-key ${{ secrets.GITHUB_TOKEN }} --source "https://nuget.pkg.github.com/shazwazza/index.json" diff --git a/Nuget.config b/Nuget.config index 0e97909..f22d6f8 100644 --- a/Nuget.config +++ b/Nuget.config @@ -1,8 +1,6 @@ - + - - - \ No newline at end of file + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a0f6ec9..02590fd 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -9,7 +9,7 @@ 9.0 - + https://github.com/Shazwazza/Smidge @@ -23,6 +23,6 @@ 4.0.0 - net7.0;net6.0;net5.0 + net8.0;net6.0; diff --git a/src/Smidge.Core/BundleManager.cs b/src/Smidge.Core/BundleManager.cs index 9398272..202b924 100644 --- a/src/Smidge.Core/BundleManager.cs +++ b/src/Smidge.Core/BundleManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using Microsoft.Extensions.Options; using Smidge.Models; @@ -202,10 +202,12 @@ public void AddToBundle(string bundleName, JavaScriptFile file) /// public Bundle GetBundle(string bundleName) { - Bundle collection; - if (!TryGetValue(bundleName, out collection)) + if (!TryGetValue(bundleName, out Bundle collection)) + { return null; + } + return collection; } } -} \ No newline at end of file +} diff --git a/src/Smidge.Core/CompositeFiles/DefaultUrlManager.cs b/src/Smidge.Core/CompositeFiles/DefaultUrlManager.cs index eed91dd..ca59c70 100644 --- a/src/Smidge.Core/CompositeFiles/DefaultUrlManager.cs +++ b/src/Smidge.Core/CompositeFiles/DefaultUrlManager.cs @@ -45,7 +45,7 @@ public string GetUrl(string bundleName, string fileExtension, bool debug, string var handler = _keepFileExtensions ? "~/{0}/{1}.{3}{4}{2}" : "~/{0}/{1}{2}.{3}{4}"; return _requestHelper.Content(string.Format(handler, _options.BundleFilePath, - Uri.EscapeUriString(bundleName), + Uri.EscapeDataString(bundleName), fileExtension, debug ? 'd' : 'v', cacheBusterValue)); @@ -167,9 +167,9 @@ private string GetCompositeUrl(string fileKey, string fileExtension, string cach string.Format( handler, _options.CompositeFilePath, - Uri.EscapeUriString(fileKey), + Uri.EscapeDataString(fileKey), fileExtension, cacheBusterValue)); } } -} \ No newline at end of file +} diff --git a/src/Smidge.Core/Smidge.Core.csproj b/src/Smidge.Core/Smidge.Core.csproj index 6e91be9..cc6c7eb 100644 --- a/src/Smidge.Core/Smidge.Core.csproj +++ b/src/Smidge.Core/Smidge.Core.csproj @@ -14,12 +14,11 @@ - - - - - - - + + + + + + diff --git a/src/Smidge.InMemory/Smidge.InMemory.csproj b/src/Smidge.InMemory/Smidge.InMemory.csproj index 699e7e0..c0a23c6 100644 --- a/src/Smidge.InMemory/Smidge.InMemory.csproj +++ b/src/Smidge.InMemory/Smidge.InMemory.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Smidge.Nuglify/NuglifySourceMapController.cs b/src/Smidge.Nuglify/NuglifySourceMapController.cs index 9d10932..aaba097 100644 --- a/src/Smidge.Nuglify/NuglifySourceMapController.cs +++ b/src/Smidge.Nuglify/NuglifySourceMapController.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Smidge.Cache; using Smidge.Models; using Smidge.Options; @@ -19,12 +20,11 @@ public NuglifySourceMapController(ISmidgeFileSystem fileSystem, IBundleManager b _bundleManager = bundleManager; } - public FileResult SourceMap([FromServices] BundleRequestModel bundle) + public ActionResult SourceMap([FromServices] BundleRequestModel bundle) { - if (!_bundleManager.TryGetValue(bundle.FileKey, out _)) + if (!bundle.IsBundleFound) { - //TODO: Throw an exception, this will result in an exception anyways - return null; + return NotFound(); } var sourceMapFile = _fileSystem.CacheFileSystem.GetRequiredFileInfo(bundle.GetSourceMapFilePath()); @@ -43,10 +43,9 @@ public FileResult SourceMap([FromServices] BundleRequestModel bundle) } } - //TODO: Throw an exception, this will result in an exception anyways - return null; + return NotFound(); } } -} \ No newline at end of file +} diff --git a/src/Smidge.Nuglify/Smidge.Nuglify.csproj b/src/Smidge.Nuglify/Smidge.Nuglify.csproj index bbcc35c..33883c7 100644 --- a/src/Smidge.Nuglify/Smidge.Nuglify.csproj +++ b/src/Smidge.Nuglify/Smidge.Nuglify.csproj @@ -10,7 +10,7 @@ true - + diff --git a/src/Smidge.Web/Startup.cs b/src/Smidge.Web/Startup.cs index 0ab919a..bf120d4 100644 --- a/src/Smidge.Web/Startup.cs +++ b/src/Smidge.Web/Startup.cs @@ -198,6 +198,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { RequestPath = "/smidge-static" }); + + bundles + .CreateCss("notfound-map-css-bundle", + "~/Css/notFoundMap.min.css" + ); }); app.UseSmidgeNuglify(); diff --git a/src/Smidge.Web/Views/Home/Index.cshtml b/src/Smidge.Web/Views/Home/Index.cshtml index 393d56b..b63f708 100644 --- a/src/Smidge.Web/Views/Home/Index.cshtml +++ b/src/Smidge.Web/Views/Home/Index.cshtml @@ -34,6 +34,7 @@ + @await SmidgeHelper.CssHereAsync("notfound-map-css-bundle", debug: false) diff --git a/src/Smidge.Web/wwwroot/Css/notFoundMap.min.css b/src/Smidge.Web/wwwroot/Css/notFoundMap.min.css new file mode 100644 index 0000000..7133bb1 --- /dev/null +++ b/src/Smidge.Web/wwwroot/Css/notFoundMap.min.css @@ -0,0 +1,2 @@ +@charset "UTF-8"; + /*# sourceMappingURL=notFound.min.css.map */ diff --git a/src/Smidge/Controllers/AddCompressionHeaderAttribute.cs b/src/Smidge/Controllers/AddCompressionHeaderAttribute.cs index f2ca3fb..f69f65d 100644 --- a/src/Smidge/Controllers/AddCompressionHeaderAttribute.cs +++ b/src/Smidge/Controllers/AddCompressionHeaderAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; @@ -55,7 +55,7 @@ public void OnActionExecuted(ActionExecutedContext context) if (context.Exception != null) return; //get the model from the items - if (context.HttpContext.Items.TryGetValue(nameof(AddCompressionHeaderAttribute), out var requestModel) && requestModel is RequestModel file) + if (context.HttpContext.Items.TryGetValue(nameof(AddCompressionHeaderAttribute), out var requestModel) && requestModel is RequestModel file && file.IsBundleFound) { var enableCompression = true; @@ -72,4 +72,4 @@ public void OnActionExecuted(ActionExecutedContext context) } } } -} \ No newline at end of file +} diff --git a/src/Smidge/Controllers/AddExpiryHeadersAttribute.cs b/src/Smidge/Controllers/AddExpiryHeadersAttribute.cs index 2929d58..cf08dae 100644 --- a/src/Smidge/Controllers/AddExpiryHeadersAttribute.cs +++ b/src/Smidge/Controllers/AddExpiryHeadersAttribute.cs @@ -50,7 +50,7 @@ public void OnActionExecuted(ActionExecutedContext context) return; //get the model from the items - if (!context.HttpContext.Items.TryGetValue(nameof(AddExpiryHeadersAttribute), out object fileObject) || fileObject is not RequestModel file) + if (!context.HttpContext.Items.TryGetValue(nameof(AddExpiryHeadersAttribute), out object fileObject) || fileObject is not RequestModel file || !file.IsBundleFound) return; var enableETag = true; diff --git a/src/Smidge/Controllers/CheckNotModifiedAttribute.cs b/src/Smidge/Controllers/CheckNotModifiedAttribute.cs index 8229ea5..8fb90eb 100644 --- a/src/Smidge/Controllers/CheckNotModifiedAttribute.cs +++ b/src/Smidge/Controllers/CheckNotModifiedAttribute.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Smidge.Models; using System; @@ -51,7 +51,7 @@ public void OnActionExecuted(ActionExecutedContext context) if (context.Exception != null) return; //get the model from the items - if (context.HttpContext.Items.TryGetValue(nameof(CheckNotModifiedAttribute), out var requestModel) && requestModel is RequestModel file) + if (context.HttpContext.Items.TryGetValue(nameof(CheckNotModifiedAttribute), out var requestModel) && requestModel is RequestModel file && file.IsBundleFound) { //Don't execute when the request is in Debug if (file.Debug) @@ -74,4 +74,4 @@ private static void ReturnNotModified(ActionExecutedContext context) } } } -} \ No newline at end of file +} diff --git a/src/Smidge/Controllers/CompositeFileCacheFilterAttribute.cs b/src/Smidge/Controllers/CompositeFileCacheFilterAttribute.cs index 674f7d0..2d3e5ec 100644 --- a/src/Smidge/Controllers/CompositeFileCacheFilterAttribute.cs +++ b/src/Smidge/Controllers/CompositeFileCacheFilterAttribute.cs @@ -68,7 +68,7 @@ public void OnActionExecuting(ActionExecutingContext context) if (context.ActionArguments.Count == 0) return; var firstArg = context.ActionArguments.First().Value; - if (firstArg is RequestModel file) + if (firstArg is RequestModel file && file.IsBundleFound) { var cacheBusterValue = file.ParsedPath.CacheBusterValue; diff --git a/src/Smidge/Controllers/SmidgeController.cs b/src/Smidge/Controllers/SmidgeController.cs index 3df51b0..f516aa4 100644 --- a/src/Smidge/Controllers/SmidgeController.cs +++ b/src/Smidge/Controllers/SmidgeController.cs @@ -1,18 +1,19 @@ using System; -using Microsoft.AspNetCore.Mvc; -using Smidge.CompositeFiles; -using Smidge.Models; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.IO.Compression; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; -using Smidge.FileProcessors; using Smidge.Cache; -using Microsoft.AspNetCore.Authorization; +using Smidge.CompositeFiles; +using Smidge.FileProcessors; +using Smidge.Models; namespace Smidge.Controllers { @@ -27,6 +28,8 @@ namespace Smidge.Controllers [AllowAnonymous] public class SmidgeController : Controller { + private static readonly ConcurrentDictionary s_locks = new ConcurrentDictionary(); + private readonly ISmidgeFileSystem _fileSystem; private readonly IBundleManager _bundleManager; private readonly IBundleFileSetGenerator _fileSetGenerator; @@ -67,51 +70,46 @@ public SmidgeController( public async Task Bundle( [FromServices] BundleRequestModel bundleModel) { - if (!_bundleManager.TryGetValue(bundleModel.FileKey, out Bundle foundBundle)) + if (!bundleModel.IsBundleFound || !_bundleManager.TryGetValue(bundleModel.FileKey, out Bundle foundBundle)) { return NotFound(); } - var bundleOptions = foundBundle.GetBundleOptions(_bundleManager, bundleModel.Debug); - - var cacheBusterValue = bundleModel.ParsedPath.CacheBusterValue; + Options.BundleOptions bundleOptions = foundBundle.GetBundleOptions(_bundleManager, bundleModel.Debug); - //now we need to determine if this bundle has already been created - var cacheFile = _fileSystem.CacheFileSystem.GetCachedCompositeFile(cacheBusterValue, bundleModel.Compression, bundleModel.FileKey, out var cacheFilePath); - if (cacheFile.Exists) + if (TryGetBundle(bundleModel, out IActionResult actionResult, out var cacheFilePath)) { - _logger.LogDebug($"Returning bundle '{bundleModel.FileKey}' from cache"); - + return actionResult; + } - if (!string.IsNullOrWhiteSpace(cacheFile.PhysicalPath)) + SemaphoreSlim bundleLock = s_locks.GetOrAdd(foundBundle.Name, s => new SemaphoreSlim(1, 1)); + await bundleLock.WaitAsync(); + try + { + // Double check, might be available now + if (TryGetBundle(bundleModel, out actionResult, out _)) { - //if physical path is available then it's the physical file system, in which case we'll deliver the file with the PhysicalFileResult - //FilePathResult uses IHttpSendFileFeature which is a native host option for sending static files - return PhysicalFile(cacheFile.PhysicalPath, bundleModel.Mime); + return actionResult; } - else + + //the bundle doesn't exist so we'll go get the files, process them and create the bundle + + //get the files for the bundle + IWebFile[] files = _fileSetGenerator.GetOrderedFileSet(foundBundle, + _processorFactory.CreateDefault( + //the file type in the bundle will always be the same + foundBundle.Files[0].DependencyType)) + .ToArray(); + + if (files.Length == 0) { - return File(cacheFile.CreateReadStream(), bundleModel.Mime); + return NotFound(); } - } - //the bundle doesn't exist so we'll go get the files, process them and create the bundle - //TODO: We should probably lock here right?! we don't want multiple threads trying to do this at the same time, we'll need a dictionary of locks to do this effectively + var cacheBusterValue = bundleModel.ParsedPath.CacheBusterValue; - //get the files for the bundle - var files = _fileSetGenerator.GetOrderedFileSet(foundBundle, - _processorFactory.CreateDefault( - //the file type in the bundle will always be the same - foundBundle.Files[0].DependencyType)) - .ToArray(); - - if (files.Length == 0) - { - return NotFound(); - } + using var bundleContext = new BundleContext(cacheBusterValue, bundleModel, cacheFilePath); - using (var bundleContext = new BundleContext(cacheBusterValue, bundleModel, cacheFilePath)) - { var watch = new Stopwatch(); watch.Start(); _logger.LogDebug($"Processing bundle '{bundleModel.FileKey}', debug? {bundleModel.Debug} ..."); @@ -123,7 +121,7 @@ public async Task Bundle( } //Get each file path to it's hashed location since that is what the pre-processed file will be saved as - var fileInfos = files.Select(x => _fileSystem.CacheFileSystem.GetCacheFile( + IEnumerable fileInfos = files.Select(x => _fileSystem.CacheFileSystem.GetCacheFile( x, () => _fileSystem.GetRequiredFileInfo(x), bundleOptions.FileWatchOptions.Enabled, @@ -131,23 +129,30 @@ public async Task Bundle( cacheBusterValue, out _)); - using (var resultStream = await GetCombinedStreamAsync(fileInfos, bundleContext)) - { - //compress the response (if enabled) - //do not compress anything if it's not enabled in the bundle options - var compressedStream = await Compressor.CompressAsync(bundleOptions.CompressResult ? bundleModel.Compression : CompressionType.None, - bundleOptions.CompressionLevel, - resultStream); - - //save the resulting compressed file, if compression is not enabled it will just save the non compressed format - // this persisted file will be used in the CheckNotModifiedAttribute which will short circuit the request and return - // the raw file if it exists for further requests to this path - await CacheCompositeFileAsync(_fileSystem.CacheFileSystem, cacheFilePath, compressedStream); + using Stream resultStream = await GetCombinedStreamAsync(fileInfos, bundleContext); - _logger.LogDebug($"Processed bundle '{bundleModel.FileKey}' in {watch.ElapsedMilliseconds}ms"); + //compress the response (if enabled) + //do not compress anything if it's not enabled in the bundle options + Stream compressedStream = await Compressor.CompressAsync(bundleOptions.CompressResult ? bundleModel.Compression : CompressionType.None, + bundleOptions.CompressionLevel, + resultStream); - //return the stream - return File(compressedStream, bundleModel.Mime); + //save the resulting compressed file, if compression is not enabled it will just save the non compressed format + // this persisted file will be used in the CheckNotModifiedAttribute which will short circuit the request and return + // the raw file if it exists for further requests to this path + await CacheCompositeFileAsync(_fileSystem.CacheFileSystem, cacheFilePath, compressedStream); + + _logger.LogDebug($"Processed bundle '{bundleModel.FileKey}' in {watch.ElapsedMilliseconds}ms"); + + //return the stream + return File(compressedStream, bundleModel.Mime); + } + finally + { + // Remove the lock from the dictionary and release the lock. + if (s_locks.TryRemove(foundBundle.Name, out SemaphoreSlim lck)) + { + lck.Release(); } } } @@ -160,7 +165,7 @@ public async Task Bundle( public async Task Composite( [FromServices] CompositeFileModel file) { - if (!file.ParsedPath.Names.Any()) + if (!file.IsBundleFound || !file.ParsedPath.Names.Any()) { return NotFound(); } @@ -201,6 +206,35 @@ public async Task Composite( } } + private bool TryGetBundle(BundleRequestModel bundleModel, out IActionResult actionResult, out string cacheFilePath) + { + var cacheBusterValue = bundleModel.ParsedPath.CacheBusterValue; + + //now we need to determine if this bundle has already been created + IFileInfo cacheFile = _fileSystem.CacheFileSystem.GetCachedCompositeFile(cacheBusterValue, bundleModel.Compression, bundleModel.FileKey, out cacheFilePath); + if (cacheFile.Exists) + { + _logger.LogDebug($"Returning bundle '{bundleModel.FileKey}' from cache"); + + + if (!string.IsNullOrWhiteSpace(cacheFile.PhysicalPath)) + { + //if physical path is available then it's the physical file system, in which case we'll deliver the file with the PhysicalFileResult + //FilePathResult uses IHttpSendFileFeature which is a native host option for sending static files + actionResult = PhysicalFile(cacheFile.PhysicalPath, bundleModel.Mime); + return true; + } + else + { + actionResult = File(cacheFile.CreateReadStream(), bundleModel.Mime); + return true; + } + } + + actionResult = null; + return false; + } + private static async Task CacheCompositeFileAsync(ICacheFileSystem cacheProvider, string filePath, Stream compositeStream) { await cacheProvider.WriteFileAsync(filePath, compositeStream); diff --git a/src/Smidge/Models/BundleRequestModel.cs b/src/Smidge/Models/BundleRequestModel.cs index 337144f..c5f3772 100644 --- a/src/Smidge/Models/BundleRequestModel.cs +++ b/src/Smidge/Models/BundleRequestModel.cs @@ -1,4 +1,4 @@ -using Smidge.CompositeFiles; +using Smidge.CompositeFiles; using System; using System.Linq; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -18,17 +18,25 @@ public BundleRequestModel(IUrlManager urlManager, IActionContextAccessor accesso // In reality we'll need to do that anyways if we want to support load balancing! // https://github.com/Shazwazza/Smidge/issues/17 + if (!IsBundleFound) + { + return; + } if (!ParsedPath.Names.Any()) { - throw new InvalidOperationException("The bundle route value does not contain a bundle name"); + IsBundleFound = false; + + return; } FileKey = ParsedPath.Names.Single(); if (!bundleManager.TryGetValue(FileKey, out Bundle bundle)) { - throw new InvalidOperationException("No bundle found with key " + FileKey); + IsBundleFound = false; + + return; } Bundle = bundle; } @@ -36,4 +44,4 @@ public BundleRequestModel(IUrlManager urlManager, IActionContextAccessor accesso public Bundle Bundle { get; } public override string FileKey { get; } } -} \ No newline at end of file +} diff --git a/src/Smidge/Models/CompositeFileModel.cs b/src/Smidge/Models/CompositeFileModel.cs index d800fc9..1d850fe 100644 --- a/src/Smidge/Models/CompositeFileModel.cs +++ b/src/Smidge/Models/CompositeFileModel.cs @@ -1,4 +1,4 @@ -using Smidge.CompositeFiles; +using Smidge.CompositeFiles; using Microsoft.AspNetCore.Mvc.Infrastructure; using Smidge.Hashing; @@ -10,10 +10,14 @@ public class CompositeFileModel : RequestModel public CompositeFileModel(IHasher hasher, IUrlManager urlManager, IActionContextAccessor accessor, IRequestHelper requestHelper) : base("file", urlManager, accessor, requestHelper) { + if (!IsBundleFound) + { + return; + } //Creates a single hash of the full url (which can include many files) FileKey = hasher.Hash(string.Join(".", ParsedPath.Names)); } public override string FileKey { get; } } -} \ No newline at end of file +} diff --git a/src/Smidge/Models/RequestModel.cs b/src/Smidge/Models/RequestModel.cs index 9704611..d439783 100644 --- a/src/Smidge/Models/RequestModel.cs +++ b/src/Smidge/Models/RequestModel.cs @@ -1,4 +1,4 @@ -using Smidge.CompositeFiles; +using Smidge.CompositeFiles; using System; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -25,7 +25,10 @@ protected RequestModel(string valueName, IUrlManager urlManager, IActionContextA ParsedPath = urlManager.ParsePath(bundleId); if (ParsedPath == null) - throw new InvalidOperationException($"Could not parse {bundleId} as a valid smidge path"); + { + IsBundleFound = false; + return; + } Debug = ParsedPath.Debug; @@ -61,5 +64,7 @@ protected RequestModel(string valueName, IUrlManager urlManager, IActionContextA public string Mime { get; private set; } public DateTime LastFileWriteTime { get; set; } + + public bool IsBundleFound { get; set; } = true; } -} \ No newline at end of file +} diff --git a/src/Smidge/Properties/launchSettings.json b/src/Smidge/Properties/launchSettings.json new file mode 100644 index 0000000..ec82ba1 --- /dev/null +++ b/src/Smidge/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Smidge": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:62980;http://localhost:62981" + } + } +} \ No newline at end of file diff --git a/src/Smidge/SmidgeHelper.cs b/src/Smidge/SmidgeHelper.cs index 3cb54a8..1135798 100644 --- a/src/Smidge/SmidgeHelper.cs +++ b/src/Smidge/SmidgeHelper.cs @@ -1,4 +1,4 @@ -using Smidge.Models; +using Smidge.Models; using System; using System.Collections.Generic; using System.Linq; @@ -173,14 +173,12 @@ private IEnumerable GenerateBundleUrlsAsync(string bundleName, string fi //TODO: We should cache this, but problem is how do we do that with file watchers enabled? We'd still have to lookup the bundleOptions // or maybe we just cache when file watchers are not enabled - probably the way to do it - var bundle = _bundleManager.GetBundle(bundleName); - if (bundle == null) - { - throw new BundleNotFoundException(bundleName); - } + var bundle = _bundleManager.GetBundle(bundleName) ?? throw new BundleNotFoundException(bundleName); if (bundle.Files.Count == 0) + { return Enumerable.Empty(); + } var result = new List(); diff --git a/src/Smidge/TagHelpers/SmidgeLinkTagHelper.cs b/src/Smidge/TagHelpers/SmidgeLinkTagHelper.cs index 239031a..fdae1ed 100644 --- a/src/Smidge/TagHelpers/SmidgeLinkTagHelper.cs +++ b/src/Smidge/TagHelpers/SmidgeLinkTagHelper.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Encodings.Web; @@ -5,43 +6,31 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Razor.TagHelpers; -using Microsoft.AspNetCore.Mvc.TagHelpers; -using System.Collections.Generic; -using System; namespace Smidge.TagHelpers { [HtmlTargetElement("link", Attributes = HrefAttributeName, TagStructure = TagStructure.WithoutEndTag)] public class SmidgeLinkTagHelper : TagHelper { - private const string HrefIncludeAttributeName = "asp-href-include"; - private const string HrefExcludeAttributeName = "asp-href-exclude"; - private const string FallbackHrefAttributeName = "asp-fallback-href"; - private const string SuppressFallbackIntegrityAttributeName = "asp-suppress-fallback-integrity"; - private const string FallbackHrefIncludeAttributeName = "asp-fallback-href-include"; - private const string FallbackHrefExcludeAttributeName = "asp-fallback-href-exclude"; - private const string FallbackTestClassAttributeName = "asp-fallback-test-class"; - private const string FallbackTestPropertyAttributeName = "asp-fallback-test-property"; - private const string FallbackTestValueAttributeName = "asp-fallback-test-value"; - private const string AppendVersionAttributeName = "asp-append-version"; private const string HrefAttributeName = "href"; + private readonly IBundleManager _bundleManager; + private readonly HtmlEncoder _encoder; - private readonly HashSet _invalid = new() + private readonly HashSet _invalidAttributes = new() { - HrefIncludeAttributeName, - HrefExcludeAttributeName, - FallbackHrefAttributeName, - FallbackHrefIncludeAttributeName, - FallbackTestClassAttributeName, - FallbackTestPropertyAttributeName, - SuppressFallbackIntegrityAttributeName, - FallbackHrefExcludeAttributeName, - FallbackTestValueAttributeName, - AppendVersionAttributeName + "asp-href-include", + "asp-href-exclude", + "asp-fallback-href", + "asp-fallback-href-exclude", + "asp-fallback-test-class", + "asp-fallback-test-property", + "asp-fallback-test-value", + "asp-suppress-fallback-integrity", + "asp-suppress-fallback-integrity", + "asp-append-version" }; + private readonly SmidgeHelper _smidgeHelper; - private readonly IBundleManager _bundleManager; - private readonly HtmlEncoder _encoder; public SmidgeLinkTagHelper(SmidgeHelper smidgeHelper, IBundleManager bundleManager, HtmlEncoder encoder) { @@ -50,10 +39,13 @@ public SmidgeLinkTagHelper(SmidgeHelper smidgeHelper, IBundleManager bundleManag _encoder = encoder; } + [HtmlAttributeName("debug")] + public bool Debug { get; set; } + /// - /// TODO: Need to figure out why we need this. If the order is default and executes 'after' the + /// TODO: Need to figure out why we need this. If the order is default and executes 'after' the /// default tag helpers like the script tag helper and url resolution tag helper, the url resolution - /// doesn't actually work, it simply doesn't get passed through. Not sure if this is a bug or if I'm + /// doesn't actually work, it simply doesn't get passed through. Not sure if this is a bug or if I'm /// doing it wrong. In the meantime, setting this to execute before the defaults works. /// public override int Order => -2000; @@ -61,9 +53,6 @@ public SmidgeLinkTagHelper(SmidgeHelper smidgeHelper, IBundleManager bundleManag [HtmlAttributeName(HrefAttributeName)] public string Source { get; set; } - [HtmlAttributeName("debug")] - public bool Debug { get; set; } - public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { if (string.IsNullOrWhiteSpace(Source)) @@ -71,7 +60,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu return; } - var exists = _bundleManager.Exists(Source); + bool exists = _bundleManager.Exists(Source); // Pass through attribute that is also a well-known HTML attribute. // this is required to make sure that other tag helpers executing against this element have @@ -83,29 +72,32 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu return; } - if (context.AllAttributes.Any(x => _invalid.Contains(x.Name))) + if (context.AllAttributes.Any(x => _invalidAttributes.Contains(x.Name))) + { + return; + } + + if (context.AllAttributes.TryGetAttribute("as", out TagHelperAttribute attribute) && attribute.Value is not "style") { - throw new InvalidOperationException("Smidge tag helpers do not support the ASP.NET tag helpers: " + string.Join(", ", _invalid)); + return; } - var result = (await _smidgeHelper.GenerateCssUrlsAsync(Source, Debug)).ToArray(); - var currAttr = output.Attributes.ToDictionary(x => x.Name, x => x.Value); - using (var writer = new StringWriter()) + var attributes = output.Attributes.ToDictionary(x => x.Name, x => x.Value); + await using (var writer = new StringWriter()) { - foreach (var s in result) + foreach (var url in await _smidgeHelper.GenerateCssUrlsAsync(Source, Debug)) { - var builder = new TagBuilder(output.TagName) - { - TagRenderMode = TagRenderMode.SelfClosing - }; - builder.MergeAttributes(currAttr); - builder.Attributes["href"] = s; + var builder = new TagBuilder(output.TagName) { TagRenderMode = TagRenderMode.SelfClosing }; + builder.MergeAttributes(attributes); + builder.Attributes["href"] = url; builder.WriteTo(writer, _encoder); } - writer.Flush(); + + await writer.FlushAsync(); output.PostElement.SetHtmlContent(new HtmlString(writer.ToString())); } + //This ensures the original tag is not written. output.TagName = null; } diff --git a/test/Smidge.Benchmarks/Smidge.Benchmarks.csproj b/test/Smidge.Benchmarks/Smidge.Benchmarks.csproj index ca714e4..d66d353 100644 --- a/test/Smidge.Benchmarks/Smidge.Benchmarks.csproj +++ b/test/Smidge.Benchmarks/Smidge.Benchmarks.csproj @@ -1,7 +1,7 @@ - + - net5.0 + net8.0 Smidge.Benchmarks Exe Smidge.Benchmarks @@ -18,9 +18,9 @@ - - - + + + diff --git a/test/Smidge.Tests/Smidge.Tests.csproj b/test/Smidge.Tests/Smidge.Tests.csproj index ed586e4..3afdc59 100644 --- a/test/Smidge.Tests/Smidge.Tests.csproj +++ b/test/Smidge.Tests/Smidge.Tests.csproj @@ -1,7 +1,7 @@ - net6.0;net5.0 + net8.0;net6.0 Smidge.Tests Smidge.Tests false @@ -14,14 +14,14 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + +