sheryever
4/18/2017 - 6:09 PM

How to create a custom BundleTransform in .NET MVC4, specifically for the purposes of not renaming variables. Since I could not find this a

How to create a custom BundleTransform in .NET MVC4, specifically for the purposes of not renaming variables. Since I could not find this after an hour of concentrated interweb searching...

// stuff above...

var bundleDt = new ScriptBundle("~/js/datatables").Include(
	"~/assets/js/plugins/datatables/jquery.dataTables.js",
	"~/assets/js/plugins/datatables/DT_bootstrap.js",
	"~/assets/js/plugins/responsive-tables/responsive-tables.js");

bundleDt.Transforms.Clear(); // remove default JsMinify
bundleDt.Transforms.Add(new JsMinifyNoRename("document")); // preserve the troublesome variable that is broken on minification
bundles.Add(bundleDt);

// stuff below...
using System;
using System.IO;
using System.Text;
using System.Web.Optimization; // the NuGet package

/// <summary>
/// Base class for transforming a list of files.  Override method <see cref="compileContents"/> to customize transform.
/// 
/// <list type="bullet">
///  <listheader>Additional reading:</listheader>
///  <item><description>http://stackoverflow.com/questions/13032721/system-web-optimization-making-function-argument-names-stay-the-same-for-certain</description></item>
///  <item><description>http://www.asp.net/mvc/tutorials/mvc-4/bundling-and-minification</description></item>
///  <item><description>http://www.dotnetexpertguide.com/2011/12/custom-transformtype-bundling.html</description></item>
///  <item><description>http://giddyrobot.com/preserving-important-comments-in-mvc-4-bundles/</description></item>
///  <item><description>http://daniel.wertheim.se/2012/11/16/customizing-the-minification-of-javascript-in-asp-net-mvc4-allowing-reserved-words/</description></item>
///  <item><description>but most importantly, ReSharper to download the source code of JsMinify</description></item>
/// </list>
/// </summary>
public abstract class StandardFileBundleTransform : /* JsMinify ,*/ IBundleTransform {
	

	protected readonly bool newlineBetweenFiles;
	protected readonly string contentType;

	/// <summary>
	/// Create a new transformer
	/// </summary>
	/// <param name="contentType">specify the output type -- can set this by default in derived types</param>
	/// <param name="newlineBetweenFiles">optionally append multiple bundle files with spacing between</param>
	protected StandardFileBundleTransform(string contentType, bool newlineBetweenFiles = false) {
		this.contentType = contentType;
		this.newlineBetweenFiles = newlineBetweenFiles;
	}

	/// <summary>
	/// Loop through the files in the response and compile their contents with <see cref="compileContents"/>
	/// </summary>
	/// <param name="context"></param>
	/// <param name="response"></param>
	public virtual void Process(BundleContext context, BundleResponse response) {
		//base.Process(context, response);

		response.ContentType = this.contentType;

		if (!this.isContextReady(context, response)) return;

		var compiled = new StringBuilder();

		foreach (var path in response.Files.Select(f => f.IncludedVirtualPath)) {
			var fullpath = context.HttpContext.Server.MapPath(path);
			using (var reader = new StreamReader(fullpath)) {
				// might as well pass the stream by reference rather than the entire contents
				// also so implementations could decide how to read the contents other than .ReadToEnd()
				if (this.newlineBetweenFiles) compiled.AppendLine(compileContents(reader, path, fullpath));
				else compiled.Append(compileContents(reader, path, fullpath));

				reader.Close();
			}
		}

		// overwrite original content with compiled version
		response.Content = compiled.ToString();
		// don't need to set caching...handled by framework automatically
	}

	/// <summary>
	/// Basic argument checking and confirming that we should process the bundle.
	/// </summary>
	/// <param name="context"></param>
	/// <param name="response"></param>
	/// <returns>true if we should continue processing, false otherwise</returns>
	protected virtual bool isContextReady(BundleContext context, BundleResponse response) {
		if (context == null)
			throw new ArgumentNullException("context");
		if (response == null)
			throw new ArgumentNullException("response");

		// don't want to do anything if not enabled
		return !context.EnableInstrumentation;
	}

	/// <summary>
	/// Implements expected transformation.
	/// </summary>
	/// <param name="source"></param>
	/// <param name="path">original relative source path</param>
	/// <param name="fullpath">fully mapped path on server</param>
	/// <remarks>If not overridden, will just return file contents</remarks>
	/// <returns></returns>
	protected virtual string compileContents(StreamReader source, string path, string fullpath) {
		return source.ReadToEnd();
	}

	/// <summary>
	/// Because the original MS version is static internal, which is dumb.  Call this after minifying engine runs in <see cref="compileContents"/>.
	/// </summary>
	/// <param name="errors">list of compiler/minifier engine errors</param>
	protected virtual string BuildErrorResults(IEnumerable<object> errors) {
		var content = new StringBuilder();
		content.Append("/* ");
		// should use `OptimizationResources.MinifyError`, but again...internal
		content.Append("MinifyError").Append(Environment.NewLine);
		foreach (object current in errors) {
			content.Append(current).Append(Environment.NewLine);
		}
		content.Append(" */").Append(Environment.NewLine);
		return content.ToString();
	}
}
/// <summary>
/// Minify js but preserve indicated variable names
/// </summary>
public class JsMinifyNoRename : ConfigurableJsMinify {
	/// <summary>
	/// Minify js but preserve indicated variable names
	/// </summary>
	/// <param name="doNotRenameThese">list of variable names to preserve</param>
	public JsMinifyNoRename(params string[] doNotRenameThese) : this(false, false, doNotRenameThese) {
	}
	/// <summary>
	/// Minify js but preserve indicated variable names
	/// </summary>
	/// <param name="newlineBetweenFiles">optionally append multiple bundle files with spacing between</param>
	/// <param name="includeFilenamesInOutput">optionally append multiple bundle files with the original file name, for added debugging</param>
	/// <param name="doNotRenameThese">list of variable names to preserve</param>
	public JsMinifyNoRename(bool newlineBetweenFiles, bool includeFilenamesInOutput, params string[] doNotRenameThese)
		: base(null, newlineBetweenFiles, includeFilenamesInOutput) {

		this.Configuration.SetNoAutoRenames(doNotRenameThese);
	}

}
using System.IO;
using Microsoft.Ajax.Utilities;


/// <summary>
/// Minify javascript files with externally overridable configuration settings.
/// </summary>
public class ConfigurableJsMinify : StandardFileBundleTransform {

	protected bool includeFilenamesInOutput;

	public CodeSettings Configuration { get; set; }

	/// <summary>
	/// Create a new minifier, optionally specifying all configuration properties.
	/// </summary>
	/// <param name="configuration">Sets the public settings <see cref="Configuration"/>.  If not provided, will use <see cref="GetStandardSettings"/> -- note that you can then override specific properties of <see cref="Configuration"/>.</param>
	/// <param name="newlineBetweenFiles">optionally append multiple bundle files with spacing between</param>
	/// <param name="includeFilenamesInOutput">optionally append multiple bundle files with the original file name, for added debugging</param>
	public ConfigurableJsMinify(CodeSettings configuration = null, bool newlineBetweenFiles = false, bool includeFilenamesInOutput = false)
		: base("text/javascript" /* would be awesome if MS exposed JsMinify.JsContentType */, newlineBetweenFiles) {

		this.includeFilenamesInOutput = includeFilenamesInOutput;
		this.Configuration = configuration ?? GetStandardSettings();
	}

	protected static Minifier compiler = new Minifier();

	/// <summary>
	/// Set up the basic minification settings, allowing you to override them later
	/// </summary>
	/// <returns></returns>
	public static CodeSettings GetStandardSettings() {
		// combining with JsMinify source code + http://outcastgeek.com/combine-minify-javascript-dotnet.html
		// also, look at explanations for command-line switches http://ajaxmin.codeplex.com/wikipage?title=Command-Line%20Switches

		return new CodeSettings {
			// copied from JsMinify source code
			EvalTreatment = EvalTreatment.MakeImmediateSafe,
			PreserveImportantComments = false, // set this to true to preserve license stuff at the beginning of files -- see http://giddyrobot.com/preserving-important-comments-in-mvc-4-bundles/
			// custom stuff
			MinifyCode = true // the most important part...explicitly tell it to minify
			// LocalRenaming = LocalRenaming.KeepAll,
			// NoAutoRenameList = "document", // this is the main troublemaker, don't try to obfuscate it
		};
	}


	protected override string compileContents(StreamReader source, string path, string fullpath) {
		//return base.compileContents(source);

		var compiled = includeFilenamesInOutput
			? "/* " + path + " */" + (this.newlineBetweenFiles ? System.Environment.NewLine : null) + compiler.MinifyJavaScript(source.ReadToEnd(), this.Configuration)
			: compiler.MinifyJavaScript(source.ReadToEnd(), this.Configuration);

		return compiler.ErrorList.Count > 0
					? BuildErrorResults(compiler.ErrorList)
					: compiled;
	}
}