Skip to content

Commit

Permalink
Working Source Map emit for JavaScriptBundle (#311)
Browse files Browse the repository at this point in the history
* Working Source Map emit for JavaScriptBundle

* Cleanup JavascriptMinifierTests to use JsSettings instead of direct use of Nuglify's CodeSettings class
  • Loading branch information
b9chris authored Jun 16, 2024
1 parent 0077e59 commit 27eca87
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 25 deletions.
11 changes: 11 additions & 0 deletions src/WebOptimizer.Core/AssetPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ public IAsset AddBundle(string route, string contentType, params string[] source
return asset;
}


public IAsset AddAsset(string route, string contentType)
{
route = NormalizeRoute(route);

IAsset asset = new Asset(route, contentType, this, new string[0]);
_assets.TryAdd(route, asset);

return asset;
}

public IEnumerable<IAsset> AddFiles(string contentType, params string[] sourceFiles)
{
if (string.IsNullOrEmpty(contentType))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public static IApplicationBuilder UseWebOptimizer(this IApplicationBuilder app)

if (app.ApplicationServices.GetService(typeof(IAssetPipeline)) == null)
{
// TODO: This error message is incorrect for Program.cs in .Net 8.0 - ConfigureServices() is retired and the call is more like services.AddWebOptimizer() now.
string msg = "Unable to find the required services. Please add all the required services by calling 'IServiceCollection.AddWebOptimizer' inside the call to 'ConfigureServices(...)' in the application startup code.";
throw new InvalidOperationException(msg);
}
Expand Down
11 changes: 11 additions & 0 deletions src/WebOptimizer.Core/IAssetPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ public interface IAssetPipeline
/// <param name="sourceFiles">A list of relative file names of the sources to optimize.</param>
IAsset AddBundle(string route, string contentType, params string[] sourceFiles);

/// <summary>
/// Add a generalized Asset, that has just a Route and a ContentType
///
/// Typically used to fill in with content details later by Processors
///
/// Used by AddJavaScriptBundle() to emit Source Maps on their own route
/// </summary>
/// <param name="route">The route that should cause the pipeline to respond with this Asset</param>
/// <param name="contentType">Content-Type of the response</param>
IAsset AddAsset(string route, string contentType);

/// <summary>
/// Adds an array of files to the optimization pipeline.
/// </summary>
Expand Down
67 changes: 67 additions & 0 deletions src/WebOptimizer.Core/Processors/ItemContentEmitter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using WebOptimizer;
using WebOptimizer.Processors;

namespace WebOptimizer.Processors
{
internal class ItemContentEmitter : Processor
{
public override Task ExecuteAsync(IAssetContext context)
{
var asset = context.Asset;
var items = asset.Items;
if (!items.ContainsKey("Content"))
return Task.CompletedTask;

context.Content = new Dictionary<string, byte[]>
{
{ "Content", ((string)items["Content"]).AsByteArray() }
};

return Task.CompletedTask;
}
}
}


namespace Microsoft.Extensions.DependencyInjection
{
public static partial class AssetPipelineExtensions
{
/// <summary>
/// Changes the Asset to only emit to Response what is stored in
/// asset.Items["Content"]
/// and nothing else
///
/// Useful for Generated Content
///
/// Used by JavaScriptMinifier.AddJavaScriptBundle to emit sourcemaps into a separate Asset
/// when generating minified code
/// </summary>
/// <param name="asset"></param>
/// <returns></returns>
public static IAsset UseItemContent(this IAsset asset)
{
asset.Processors.Add(new ItemContentEmitter());
return asset;
}

/// <summary>
/// Changes the Asset to only emit to Response what is stored in
/// asset.Items["Content"]
/// and nothing else
///
/// Useful for Generated Content
///
/// Used by JavaScriptMinifier.AddJavaScriptBundle to emit sourcemaps into a separate Asset
/// when generating minified code
/// </summary>
public static IEnumerable<IAsset> UseItemContent(this IEnumerable<IAsset> assets)
{
return assets.AddProcessor(asset => asset.UseItemContent());
}
}
}
94 changes: 75 additions & 19 deletions src/WebOptimizer.Core/Processors/JavaScriptMinifier.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NUglify;
using NUglify.JavaScript;
using WebOptimizer;
using WebOptimizer.Processors;

namespace WebOptimizer
{
internal class JavaScriptMinifier : Processor
{
public JavaScriptMinifier(CodeSettings settings)
public JavaScriptMinifier(JsSettings settings)
{
Settings = settings;
}

public CodeSettings Settings { get; set; }
public JsSettings Settings { get; set; }

public override Task ExecuteAsync(IAssetContext config)
{
if (!Settings.MinifyCode) return Task.CompletedTask;
if (!Settings.CodeSettings.MinifyCode) return Task.CompletedTask;
var content = new Dictionary<string, byte[]>();

foreach (string key in config.Content.Keys)
Expand All @@ -30,14 +34,52 @@ public override Task ExecuteAsync(IAssetContext config)

string input = config.Content[key].AsString();
string minified;

try
{
UglifyResult result = Uglify.Js(input, Settings);
UglifyResult result;
string sourceMapContent = null;

// If .AddJavascriptBundle setup the SourceMap Asset, it will be assigned here, we need to fill it
var sourceMapAsset = Settings.PipelineSourceMap;
if (sourceMapAsset != null)
{
// Setup the side-effects writing of the SourceMap file
var sb = new StringBuilder();
using (var sw = new StringWriter(sb))
{
using (var sourceMap = new V3SourceMap(sw))
{
// Causes the side-effect writing of the SourceMap to our StringWriter...
Settings.CodeSettings.SymbolsMap = sourceMap;
sourceMap.MakePathsRelative = false;
sourceMap.StartPackage(config.Asset.Route, sourceMapAsset.Route);

result = Uglify.Js(input, Settings.CodeSettings);
}
// These Dispose steps cause the actual flush of the content to the StringBuilder
}
sourceMapContent = sb.ToString();
}
else
{
result = Uglify.Js(input, Settings.CodeSettings);
}

minified = result.Code;

if (result.HasErrors)
{
minified = $"/* {string.Join("\r\n", result.Errors)} */\r\n" + input;
}
else
{
if (sourceMapContent != null)
{
// Successful minification, and source map generation succeeded, write out to its separate Asset/Route
sourceMapAsset.Items["Content"] = sourceMapContent;
}
}
}
catch
{
Expand Down Expand Up @@ -75,21 +117,21 @@ public static IEnumerable<IAsset> MinifyJsFiles(this IAssetPipeline pipeline)
/// </summary>
public static IEnumerable<IAsset> MinifyJsFiles(this IAssetPipeline pipeline, CodeSettings settings)
{
return pipeline.MinifyJsFiles(settings, "**/*.js");
return pipeline.MinifyJsFiles(new JsSettings(settings), "**/*.js");
}

/// <summary>
/// Minifies the specified .js files.
/// </summary>
public static IEnumerable<IAsset> MinifyJsFiles(this IAssetPipeline pipeline, params string[] sourceFiles)
{
return pipeline.MinifyJsFiles(new CodeSettings(), sourceFiles);
return pipeline.MinifyJsFiles(new JsSettings(), sourceFiles);
}

/// <summary>
/// Minifies tje specified .js files.
/// Minifies the specified .js files.
/// </summary>
public static IEnumerable<IAsset> MinifyJsFiles(this IAssetPipeline pipeline, CodeSettings settings, params string[] sourceFiles)
public static IEnumerable<IAsset> MinifyJsFiles(this IAssetPipeline pipeline, JsSettings settings, params string[] sourceFiles)
{
return pipeline.AddFiles("text/javascript; charset=UTF-8", sourceFiles)
.AddResponseHeader("X-Content-Type-Options", "nosniff")
Expand All @@ -101,33 +143,47 @@ public static IEnumerable<IAsset> MinifyJsFiles(this IAssetPipeline pipeline, Co
/// </summary>
public static IAsset AddJavaScriptBundle(this IAssetPipeline pipeline, string route, params string[] sourceFiles)
{
return pipeline.AddJavaScriptBundle(route, new CodeSettings(), sourceFiles);
return pipeline.AddJavaScriptBundle(route, new JsSettings(), sourceFiles);
}

/// <summary>
/// Creates a JavaScript bundle on the specified route and minifies the output.
/// </summary>
public static IAsset AddJavaScriptBundle(this IAssetPipeline pipeline, string route, CodeSettings settings, params string[] sourceFiles)
public static IAsset AddJavaScriptBundle(this IAssetPipeline pipeline, string route, JsSettings settings, params string[] sourceFiles)
{
return pipeline.AddBundle(route, "text/javascript; charset=UTF-8", sourceFiles)
.EnforceFileExtensions(".js", ".jsx", ".es5", ".es6")
.Concatenate()
.AddResponseHeader("X-Content-Type-Options", "nosniff")
.MinifyJavaScript(settings);
var bundleAsset = pipeline.AddBundle(route, "text/javascript; charset=UTF-8", sourceFiles)
.EnforceFileExtensions(".js", ".jsx", ".es5", ".es6")
.Concatenate()
.AddResponseHeader("X-Content-Type-Options", "nosniff")
.MinifyJavaScript(settings);

if (settings.GenerateSourceMap)
{
// A simple config flag saying to generate a SourceMap - the legwork is on the framework
// Nuglify returns minified Javascript while generating a SourceMap as a side effect, like it or not, and
// It's not possible to ask for a map until the first time the minified code is delivered (since the map is in the comments of the min bundle),
// so we add a null route/Asset to the pipeline for now, and we'll fill it in later on first request of the bundle
string mapRoute = route.Replace(".js", ".map.js");
var sourceMapAsset = pipeline.AddAsset(mapRoute, "application/json")
.UseItemContent();
settings.PipelineSourceMap = sourceMapAsset;
}

return bundleAsset;
}

/// <summary>
/// Runs the JavaScript minifier on the content.
/// </summary>
public static IAsset MinifyJavaScript(this IAsset asset)
{
return asset.MinifyJavaScript(new CodeSettings());
return asset.MinifyJavaScript(new JsSettings());
}

/// <summary>
/// Runs the JavaScript minifier on the content.
/// </summary>
public static IAsset MinifyJavaScript(this IAsset asset, CodeSettings settings)
public static IAsset MinifyJavaScript(this IAsset asset, JsSettings settings)
{
var minifier = new JavaScriptMinifier(settings);
asset.Processors.Add(minifier);
Expand All @@ -140,13 +196,13 @@ public static IAsset MinifyJavaScript(this IAsset asset, CodeSettings settings)
/// </summary>
public static IEnumerable<IAsset> MinifyJavaScript(this IEnumerable<IAsset> assets)
{
return assets.MinifyJavaScript(new CodeSettings());
return assets.MinifyJavaScript(new JsSettings());
}

/// <summary>
/// Runs the JavaScript minifier on the content.
/// </summary>
public static IEnumerable<IAsset> MinifyJavaScript(this IEnumerable<IAsset> assets, CodeSettings settings)
public static IEnumerable<IAsset> MinifyJavaScript(this IEnumerable<IAsset> assets, JsSettings settings)
{
return assets.AddProcessor(asset => asset.MinifyJavaScript(settings));
}
Expand Down
40 changes: 40 additions & 0 deletions src/WebOptimizer.Core/Processors/JsBundleSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text;


namespace WebOptimizer.Processors
{
public class JsSettings
{
/// <summary>
/// Defaults to false.
/// Whether to generate source maps, allowing bundled code to be debugged using original source in places like Chrome Dev Tools.
/// Respects .SymbolsMap on base class, CodeSettings; this setting is ignored (treated as false) if caller sets .SymbolsMap
/// </summary>
public bool GenerateSourceMap { get; set; }

/// <summary>
/// Set by the framework if GenerateSourceMap is true
/// Helps a given Bundle Asset identify its SourceMap Asset to write to.
/// </summary>
public IAsset PipelineSourceMap { get; set; }

/// <summary>
/// NUglify is the underlying minifier for WebOptimizer.
/// It's derived from Microsoft's AjaxMin.
/// </summary>
public NUglify.JavaScript.CodeSettings CodeSettings { get; set; }



public JsSettings()
{
CodeSettings = new NUglify.JavaScript.CodeSettings();
}
public JsSettings(NUglify.JavaScript.CodeSettings nuglifyCodeSettings)
{
CodeSettings = nuglifyCodeSettings;
}
}
}
3 changes: 2 additions & 1 deletion src/WebOptimizer.Core/TagHelpersDynamic/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.DependencyInjection;
using WebOptimizer.Processors;

namespace WebOptimizer.TagHelpersDynamic
{
Expand Down Expand Up @@ -91,7 +92,7 @@ internal static IAsset CreateJsAsset(IAssetPipeline pipeline, string key)

if (settings.Minify)
{
asset = asset.MinifyJavaScript(settings.CodeSettings);
asset = asset.MinifyJavaScript(new JsSettings(settings.CodeSettings));
}

return asset;
Expand Down
2 changes: 1 addition & 1 deletion src/WebOptimizer.Core/WebOptimizer.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@
<Pack>true</Pack>
</Content>
<None Include="../../README.md" Pack="true" PackagePath="" />
<None Include="../../art/logo64x64.png" Pack="true" Visible="false" PackagePath="logo.png"/>
<None Include="../../art/logo64x64.png" Pack="true" Visible="false" PackagePath="logo.png" />
</ItemGroup>
</Project>
Loading

0 comments on commit 27eca87

Please sign in to comment.