Javascript runtime compilation using Asp.Net and Google's Closure Compiler

by Guido Tapia

in software-engineering,

December 10, 2009

Overview

Working on complex javascript projects usually means working with lots of javascript files. When it comes time to deploy this to production it can be be very tedious and sometimes dangerous to do this manually. This solution compiles all the javascript files in a directory to a single minified file ideal for release. This code will also automatically recompile this file if any of the files change.

Code

///
 /// This class will compile (using Google's Closure Compiler) a directory of javascript files if: /// - In Release Mode /// - The release (minified) file is older than other files in the directory (stale) /// - Not on localhost (Google cannot access your files) /// 
public class JavascriptRuntimeCompiler { private static readonly ILog log = LogUtil.Logger(typeof (JavascriptRuntimeCompiler)); private const string CACHE_MARKER = "CACHE_MARKER"; private readonly string releaseFileName; private readonly string baseScriptsUri; private readonly HttpContextBase context; private FileInfo releaseFile; ///
/// Creates the JavascriptRuntimeCompiler. It is safe to create this per request as it is very efficient and /// will only create the new minified file if required. ///
 
/// The relative file name of the release minified file. Eg: 'mydir\scripts\myscript.min.js' /// The relative uri of the directory holding the javascript files. Eg: '~/mydir/scripts/'. It is safe /// for this to be the same directory as the one holding the release file. /// /// The HttpContextBase object (Asp.Net Mvc) public JavascriptRuntimeCompiler(string releaseFileName, string baseScriptsUri, HttpContextBase context) { this.releaseFileName = context.Request.PhysicalApplicationPath + releaseFileName; this.baseScriptsUri = context.Request.Url.GetLeftPart(UriPartial.authorsity) + baseScriptsUri; this.context = context; } ///
/// Wether to use the release script or not. If true ensure your page points to your release file. If false /// your page should reference all of the debug scripts. ///
 
public bool UseReleaseScript() { bool debug = false; #if (DEBUG) debug = true; #endif if (debug || baseScriptsUri.IndexOf("localhost") >= 0) { return false; } CheckReleaseScriptValidity(); return true; } private void CheckReleaseScriptValidity() { releaseFile = new FileInfo(releaseFileName); if (releaseFile.Exists && !IsReleaseFileOutOfDate()) { return; } RebuildReleaseScriptFile(); // This marker allows us to do a date check on all the files that the minified release file depends on context.Cache.Add(CACHE_MARKER, new Object(), new CacheDependency(GetJSFiles()), Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); } private bool IsReleaseFileOutOfDate() { if (context.Cache[CACHE_MARKER] != null) { return false; } DateTime releaseFileDate = releaseFile.LastWriteTime; foreach (string s in GetJSFiles()) { if (File.GetLastWriteTime(s) > releaseFileDate) { return true; } } return false; } private void RebuildReleaseScriptFile() { string uri = "http://closure-compiler.appspot.com/compile?compilation_level=SIMPLE_OPTIMIZATIONS&output_format=text&output_info=compiled_code"; foreach (string f in GetJSFiles()) { uri += "&code_url=" + baseScriptsUri + f.Substring(f.LastIndexOf("\\") + 1); } log.Debug("Requesting Compiler: " + uri); WebRequest r = WebRequest.Create(uri); r.Method = "POST"; r.ContentLength = 0; using (Stream s = r.GetResponse().GetResponseStream()) { using (StreamReader sr = new StreamReader(s, Encoding.UTF8)) { string content = sr.ReadToEnd(); log.Info(content); FileUtils.WriteFileContents(releaseFileName, Encoding.UTF8.GetBytes(content)); } } } private static string[] cached_js_files; private string[] GetJSFiles() { if (cached_js_files != null) return cached_js_files; List jsFiles = new List(); if (releaseFile.Directory == null) throw new ApplicationException(); foreach (FileInfo f in releaseFile.Directory.GetFiles()) { if (f.Name.Equals(releaseFile.Name) || f.Extension != ".js") { continue; } jsFiles.Add(f.FullName); } return cached_js_files = jsFiles.ToArray(); } }

Using this Class

This example uses Asp.Net Mvc and the Spark view engine. However it should be trivial to change the above file or this example to use any other framework.

<if condition="new JavascriptRuntimeCompiler('resources\\scripts\\custom\\scripts.min.js', Url.Content('~/resources/scripts/custom/'), Context).UseReleaseScript()">
    <script language="javascript" src="${ Url.Content('~/resources/scripts/custom/scripts.min.js') }"></script>            
</if>
<else>
    <script language="javascript" src="${ Url.Content('~/resources/scripts/custom/Util.js') }"></script>    
    <script language="javascript" src="${ Url.Content('~/resources/scripts/custom/Class1.js') }"></script>    
    <script language="javascript" src="${ Url.Content('~/resources/scripts/custom/Class2.js') }"></script>    
    <script language="javascript" src="${ Url.Content('~/resources/scripts/custom/Class3.js') }"></script>                
</else>

Thanks

Guido Tapia Software Development Manager PicNet Pty Ltd