Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Working Source Map emit for JavaScriptBundle #310

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
7 changes: 5 additions & 2 deletions src/WebOptimizer.Core/WebOptimizer.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NUglify" Version="1.20.7" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)'== 'netcoreapp3.0'">
Expand All @@ -43,6 +42,10 @@
<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>

<ItemGroup>
<ProjectReference Include="..\..\..\Nuglify\src\NUglify\NUglify.csproj" />
</ItemGroup>
</Project>