Sponsored By Aspose - File Format APIs for .NET

Aspose are the market leader of .NET APIs for file business formats – natively work with DOCX, XLSX, PPT, PDF, MSG, MPP, images formats and many more!

Compressing JS files as part of your build process

We love jQuery – many things are now easier/cleaner/more testable using JavaScript rather than ASP.NET (that statement blows my mind, but it’s true!). Because of that, we have a lot of JavaScript code. Ideally, we want to develop using readable JavaScript files (even 3rd party files) and deploy compressed/obfuscated code. Our solution has simply been to apply a post-build task to our project. Here’s what we did.


First, we use the .NET port of Yahoo’s YUI Compressor, freely available on CodePlex. (note, the YUICompressor comes with an MSBuild Task, which you can use in addition (or as an alternative) to my approach).


We took the library and wrapped it into a simple console application which takes 2 inputs – the input directory containing the debug javascript files, and the target output directory to place the compressed files in:

using System;
using System.IO;
using Yahoo.Yui.Compressor;

public class Program
{
public static void Main(string[] args)
{
var input = args[0];
var output = args[1];

if (!Directory.Exists(output))
{
throw new ArgumentException(string.Format(“The output directory ‘{0}’ doesn’t exist”, output));
}
if (Directory.Exists(input))
{
CompressDirectory(input, output);
}
else if (File.Exists(input))
{
CompressFile(input, output);
}
else
{
throw new ArgumentException(string.Format(“{0} isn’t a known directory or file”, input));
}
}
public static void CompressDirectory(string root, string outputDirectory)
{
Console.WriteLine(“Compressing all .js files within: ” + root);
foreach (var file in Directory.GetFiles(root, “*.js”))
{
CompressFile(file, outputDirectory);
}
foreach (var directory in Directory.GetDirectories(root))
{
//skip .svn folders and what not
if ((new DirectoryInfo(directory).Attributes & FileAttributes.Hidden) != FileAttributes.Hidden)
{
var newOuputDirectory = outputDirectory + directory.Substring(root.Length) + “\\”;
CompressDirectory(directory, newOuputDirectory);
}
}
}
public static void CompressFile(string file, string outputDirectory)
{
var name = Path.GetFileName(file);
Console.WriteLine(“Compressing file: ” + name);
using (var sw = new StreamWriter(outputDirectory + name))
using (var sr = new StreamReader(file))
{
var compressed = JavaScriptCompressor.Compress(sr.ReadToEnd(), false);
sw.Write(compressed);
}
}
}


We compile our console application and place the .exe somewhere relative to our main solution. Next, we open our project’s properties, go into the Build Events tab, and enter the following into the “Post-build event command line” box:

$(SolutionDir)..\Tools\YUICompressor\YUICompressor.exe $(ProjectDir)\content\js\debug\ $(ProjectDir)\content\js\release\

Now, whenever we build, all of our .js files within content/js/debug/ get compressed and moved to content/js/release/.


The last step is to use an HtmlHelper expression method to include our javascript file:

public static string IncludeJs(this HtmlHelper helper, string javascriptFile)
{
#if DEBUG
var subfolder = “debug”;
#else
var subfolder = “release”;
#endif
var path = string.Format(“{0}/Content/js/{1}/{2}.js”, _applicationPath, subfolder, javascriptFile);
return string.Format(“<script type=\”text/javascript\” src=\”{0}\”></script>”, path);
}

Which can simply be used like:

<%= Html.IncludeJs(“jquery”) %>
<%= Html.IncludeJs(“main”) %>
<%= Html.IncludeJs(“jquery_validate”) %>

This entry was posted in Uncategorized. Bookmark the permalink. Follow any comments here with the RSS feed for this post.

11 Responses to Compressing JS files as part of your build process

  1. Shay Jacoby says:

    Thanks for the lib, I was 1 step before installing java jvm in order to use the yahoo original java jar.

    In you console application code above the directories are not created as they should.
    It must create automatically target folders if not exists, I modified the code to:

    using System;
    using System.IO;
    using Yahoo.Yui.Compressor;

    namespace YuiCompressor
    {

    public class Program
    {

    ///

    /// $(SolutionDir)Tools\YUICompressor\bin\Debug\YUICompressor.exe $(ProjectDir)Scripts\ $(ProjectDir)Scripts\release\
    ///

    /// /// ArgumentException.
    public static void Main(string[] args)
    {
    /*
    args = new string[2];
    args[0] = @”D:\www\Framework\vr\Scripts\”;
    args[1] = @”D:\www\Framework\vr\Scripts.Publish\”;
    */
    var input = args[0];
    var output = args[1];

    if (!Directory.Exists(output))
    {
    //throw new ArgumentException(string.Format(“The output directory ‘{0}’ doesn’t exist”, output));
    Console.WriteLine(“Create Directory: ” + output);
    Directory.CreateDirectory(output);
    }
    if (Directory.Exists(input))
    {
    CompressDirectory(input, output);
    }
    else if (File.Exists(input))
    {
    CompressFile(input, output);
    }
    else
    {
    throw new ArgumentException(string.Format(“{0} isn’t a known directory or file”, input));
    }

    Console.WriteLine(“Finished to compress all .js files.”);

    }
    public static void CompressDirectory(string root, string outputDirectory)
    {
    if (!Directory.Exists(outputDirectory))
    {
    //throw new ArgumentException(string.Format(“The output directory ‘{0}’ doesn’t exist”, output));
    Console.WriteLine(“Create Directory: ” + outputDirectory);
    Directory.CreateDirectory(outputDirectory);
    }

    Console.WriteLine(“Compressing all .js files within: ” + root);
    foreach (var file in Directory.GetFiles(root, “*.js”))
    {
    CompressFile(file, outputDirectory);
    }
    foreach (var directory in Directory.GetDirectories(root))
    {
    //skip .svn folders and what not
    if ((new DirectoryInfo(directory).Attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
    continue;
    var newOuputDirectory = outputDirectory + directory.Substring(root.Length) + “\\”;
    CompressDirectory(directory, newOuputDirectory);
    }
    }
    public static void CompressFile(string file, string outputDirectory)
    {
    var name = Path.GetFileName(file);
    Console.WriteLine(“Compressing file: ” + name);
    using (var sw = new StreamWriter(outputDirectory + name))
    using (var sr = new StreamReader(file))
    {
    var compressed = JavaScriptCompressor.Compress(sr.ReadToEnd(), false);
    sw.Write(compressed);
    }
    }

    /*
    public static string IncludeJs(this HtmlHelper helper, string javascriptFile)
    {
    #if DEBUG
    var subfolder = “debug”;
    #else
    var subfolder = “release”;
    #endif
    var path = string.Format(“{0}/Content/js/{1}/{2}.js”, _applicationPath, subfolder, javascriptFile);
    return string.Format(““, path);
    }
    < %= Html.IncludeJs("jquery") %>
    < %= Html.IncludeJs("main") %>
    < %= Html.IncludeJs("jquery_validate") %>
    */

    }

    }

  2. Pure Krome says:

    @Karl: Kewl :) I’m stoked that you like (and are even using) the port i did! I was so surprised that no one had any .NET code or even a port of something existing! I found that lots of people just installed Java and integrated that into their build process. EEEEK!!! I couldn’t think of anything worse! Anyways, that’s the reason why i did it and dropping 12 or so js files down to one, as well as minifying them was a great speed improvement to our site.

    @ Timothy : exactly dude. I couldn’t agree more, regardless if you use my port or the original or someone elses.

    Keep smiling :)

  3. Timothy says:

    I’m not much of a fan of JQuery (MooTools all the way! Or at least Prototype…). But, yea, YUICompressor should be used in every project.

    In fact, everyone should get the YSlow extension for Firebug to see what can be done to improve performance.

  4. Peter Mounce says:

    Your Html.IncludeJs registers a single script-block per file? You’ll get significantly better performance (both in terms of number of requests going down, and in terms of compression efficiency) if you, in release mode, include one set of javascripts minified and globbed together into a single file. See http://www.julienlecomte.net/blog/2007/08/13/ and compar your approach as now to a one-minified-file approach with Firebug/YSlow.

    You can do the one-minified-file approach by changing the helper to add the path to a set (as in, set-semantics list), and then (I’m coming from MonoRail, not MS-MVC) write out the script includes as an after-rendering filter – in debug mode, write out one include per file, linking against the debug copies; in release mode, write out a single include that links against the set of scripts, minified.

    Except you can’t do that (well, you _can_, but… 😉 ) with a build-time task, because it would need to know all of the possible combinations of script-includes that your app uses. I think in this case if you care enough to be compressing in the first place (ie, you need to scale / want responsive UI), you can probably eat the production-CPU-cycles and be better off doing this at run-time, not build-time.

    For interest, there is a most-of-the-way-there patch to MonoRail that creates a ViewComponent that does most of this – it lacks a debug-mode switch, currently. You can see that at http://support.castleproject.org/projects/MR/issues/view/MR-ISSUE-457.

  5. Have you seen Packer for .NET?

    It provides MSBuild tasks and can compress with Packer or JSMin modes. Recently I got a contribution of CSSMinify to the project so you can compress CSS as well now. You can either do it at build time or use the assembly to do it at runtime.

    http://www.smallsharptools.com/Projects/Packer/

  6. karl says:

    @jerry:
    Nope, didn’t know about it. I don’t use VS.NET for JS editing, find it crappy. Plain text editor for me. But should be easy to change anything around to fit your particular set-up.

  7. Jerry says:

    Nice post.

    Did you look into incorporating the -vsdoc.js/.debug.js hotfix (http://blogs.msdn.com/webdevtools/archive/2008/11/07/hotfix-to-enable-vsdoc-js-intellisense-doc-files-is-now-available.aspx)?

    I’d want to name my files that I edit foo.debug.js then compress them to foo.js. Then the hotfix takes over and includes the right file. Added advantage is that this can also be used in html files.

    Then you could get rid of your IncludeJS helper.

  8. Johan says:

    Truly great, thanks.

  9. Paco says:

    I wish you posted this last week! I didn’t know about the .Net port and I spent a too much time figuring out how to integrate the Java version…

  10. Max Pool says:

    Well done – I need the exact same thing and have been putting it off for awhile. Guess I have no excuse now but to get to work.

  11. Stephen says:

    Great idea, I think its nice to show people just how simple some things are to achieve.. sometimes we can forget that being software developers means we can .. write software to help ourselves.