Saturday, March 17, 2012

Disable Minification with MVC 4 Bundles

I decided to play around with a bunch of new technologies to dive in and learn today. I decided to write a basic blog app (yeah, boring, I know) using the following tools:
    (TL;DR? Click Here)


The Problems

Everything was going well, adding NuGet packages left and right (click, click, click!)
I added my JsTrace MVC NuGet package and started to get JavaScript errors.It turns out that I had 2 problems with the Bundles feature:
  1. It doesn't add anything to the Bundles automatically except the original scripts in the Template.
  2. It automatically uses minified files by looking for "min" or not "debug" JavaScript or CSS files.

Fix #1 - Add my own Bundles

Grr!  To fix #1, I had to add my own Bundle to the Global.asax:
Bundle tomsBundle = new Bundle("~/core/js");
tomsBundle.AddDirectory("~/Scripts", "Trace*", false, true);
tomsBundle.AddDirectory("~/Scripts/Core", "Foo.*", false, true);
BundleTable.Bundles.Add(tomsBundle);
OK, problem solved.

Fix #1a - Bootstrap Bundles

While I was there, I decided to make an extension method to add the Twitter.Bootstrap stuff to the BundleTable:
public static class BootstrapExtensionMethods
{
    public static void EnableBootstrapBundle(
        this BundleCollection bundles)
    {
        var bootstrapCss = new Bundle("~/bootstrap/css", 
            new CssMinify());
        bootstrapCss.AddDirectory("~/Content", "bootstrap*", 
            false, true);
        bundles.Add(bootstrapCss);
 
        var bootstrapJs = new Bundle("~/bootstrap/js", new JsMinify());
        bootstrapJs.AddFile("~/Scripts/bootstrap.js");
        bundles.Add(bootstrapJs);
    }
}
So, add a one-liner to the Global.asax:
BundleTable.Bundles.EnableBootstrapBundle();

Fix #2 - Supersize Me!

Now, for Problem #2 – I want to have big, fat un-minimized debug versions of my files while I'm developing.ILSpy_JsMinify So, after digging around with Google and ILSpy, I came across the implementation of JsMinify and noticed that it uses a property called "EnableInstrumentation" to determine whether or not it should minify the files. Eureka!

Of course, they don't actually provide you with a simple way to set that flag. So, I realized I had to create my own implementation of 2 interfaces:
  • IBundleBuilder - has the responsibility for iterating over a collection of files and combining them
  • IBundleTransform – has the responsibility of "transforming" (i.e. minifying) the bundles (default implementations being JsMinify and CssMinify)
So, I created two wrapper classes (basically, the Decorator Pattern):
  • NonMinifyingBundleBuilder – reverses Microsoft's logic and replaces the minified files with their debug counterparts.
  • InstrumentedBundleTransform – sets the "EnableInstrumentation" flag to true while processing.
I then added a simple extension method to override everything in the BundleTable:
BundleTable.Bundles.DisableMinify();
This is probably best wrapped in an "#if DEBUG" so your release code still gets all minified.
In fact, that's what I do in the NuGet package I created out of this!

NOTE: this is a beta (since it relies on Microsoft.Web.Optimization 1.0.0-beta), so you will have to install the NuGet package with the NuGet Package Console like this:
PM> Install-Package McKearney.BundlingTweaks -Pre

8 comments:

  1. Where in the Global.asax.cs file does the bundle registration code belong?

    ReplyDelete
  2. Never mind, I figured it out. However, I've discovered a new problem. In bootstrap.css you have:

    ...
    background-image: url("images/glyphicons-halflings.png");
    ...

    When the browser attempts to resolve this, the path is:

    /bootstrap/images/glyphicons-halflings.png

    which results in a 404. So, how do you resolve this with the bundles, WITHOUT editing the .css? Thanks!

    ReplyDelete
  3. OK. Problems fixed. The issue resides in how the bundles are created. I changed the code to this:

    //bootstrap
    var bootstrapCss = new Bundle("~/Content/css", new CssMinify());
    bootstrapCss.AddFile("~/Content/bootstrap.css");
    bootstrapCss.AddFile("~/Content/bootstrap-responsive.css");
    BundleTable.Bundles.Add(bootstrapCss);

    var bootstrapJs = new Bundle("~/Content/js", new JsMinify());
    bootstrapJs.AddFile("~/Scripts/bootstrap.js");
    BundleTable.Bundles.Add(bootstrapJs);

    and in my HTML:

    <script src="@BundleTable.Bundles.ResolveBundleUrl("~/Scripts/js")" type="text/javascript"></script>
    <link href="@BundleTable.Bundles.ResolveBundleUrl("~/Content/css")" rel="stylesheet" type="text/css" />
    <script src="@BundleTable.Bundles.ResolveBundleUrl("~/Content/js")" type="text/javascript"></script>

    It's important that jQuery is added BEFORE the bootstrap JS. Also, in order for bootstrap-responsive.css to function properly, the CSS file must be added AFTER the base bootstrap.css code. Adding all the bootstrap CSS files using AddDirectory creates a bundle with the CSS out of order.

    ReplyDelete
  4. Just got back from vacation. Thanks for finding and fixing this stuff. I will try to incorporate the changes very soon.

    Tom

    ReplyDelete
    Replies
    1. One other note. The Bootstrap fluid example has some additional CSS in the <head> tag:

      <style>
      body
      {
      padding-top: 60px;
      padding-bottom: 40px;
      }
      .sidebar-nav
      {
      padding: 9px 0;
      }
      </style>

      that goes BETWEEN the bootstrap.css and bootstrap-responsive.css tags. I just put that into a file, bootstrap-tweak.css and sandwiched it between the AddFile methods:

      bootstrapCss.AddFile("~/Content/bootstrap.css");
      bootstrapCss.AddFile("~/Content/bootstrap-tweak.css");
      bootstrapCss.AddFile("~/Content/bootstrap-responsive.css");

      Delete
    2. Would you prefer to just give me a pull request with your fixes? :)

      Delete
  5. Sure. Where is your project hosted? Link?

    ReplyDelete
  6. The code's here..

    http://bundling.codeplex.com/

    Let me know if you have any problems with it.

    Tom

    ReplyDelete