Thursday, February 7, 2008

Auto generate release scripts for web application using T4 template

Download source code for this article.

 

New version of VS 2008 supports advanced intellisense features, and this is definitely good. I think you are aware of this fact, if not read ScottGu's perfect article first.

Intellisense also takes care of displaying associated comments of a js functions

Take a look:

 

image

 

With these enhancements developers are encouraged to comment every javascript function, because now it helps more then before and we have the ability of maintaining well-documented code and the same time very usable.

But before releasing the script we need to remove all comments and white-spaces, to shrink the size of the files. So I want to automate this process.

To accomplish this I will use 3 files that are available from AjaxControlToolkit project:

image

They are located in Binaries sub-folder. Most of all I am interested with static "Strip" method of "JavaScriptCommentStripper" class in JavaScriptCommentStripper.dll assembly. So I am going to use this method to create release versions of all my scripts in web application. You can also search for how AjaxControlToolkit community uses the dll, I want to say shortly that the method you see here works even for scripts in simple web folder, they have not to be compiled as web resources etc.

To automate the process I will use T4 templating engine that is built into Visual Studio 2008. Please have a look of my previous article to see more detailed explanation how to use it. There are also some useful links and comments, so take a look of it.

Today I will write a little different template, because actually it will generate many files instead of one!

Lets see the picture, suppose we have a directory structure that looks like:

image

and after running transformation I want to have it look like:

image

Contents of files,

Before transformation (Script.debug.js - 6kb):

image

After transformation(Script.js - 4kb):

image

After this we can easily use nice feature of ASP.NET AJAX ScriptManager to specify the script to include, it will append .debug on the end when in debug mode:

<asp:ScriptManager 
    runat="server"
    ScriptMode="Release"
    ID="SM">
    <Scripts>
        <asp:ScriptReference ScriptMode="Inherit" Path="~/Scripts/Script.js" />
    </Scripts>
</asp:ScriptManager>

Source of page with ScriptManager running in Release mode:

<script src="Scripts/Script.js" type="text/javascript"></script>

Source of page with ScriptManager running in Debug mode:

<script src="Scripts/Script.debug.js" type="text/javascript"></script>

Ok, lets see what does this magic happen.

1) Create file in Class Library Project named "CommentStripper.tt".

2) Then add the files JavaScriptCommentStripper.dll, Tools.dll, Tools.netmodule to the same directory where template is.

3) Paste the following code into template's contents:

<#@ output extension=".cs" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Reflection" #>
<#@ template hostspecific="True" #>
<#
Initialize(Host.ResolvePath(@""), Host.ResolvePath(@"..\Auto_generating_strong_typed_navigation_class"));
Process();
#>
<#+ 
    Assembly assembly;
    MethodInfo stripMethod;
    String webRootPath;
    const string extension = "*.js";
    
    public     void Initialize(string reflectionDir, string webRoot)
    {
        webRootPath = webRoot;
        string assemblyPath = System.String.Format(
            @"{0}\JavaScriptCommentStripper.dll", reflectionDir
        );
        assembly = Assembly.LoadFrom(assemblyPath);
        stripMethod = assembly
            .GetType("AjaxControlToolkit.JavaScriptCommentStripper")
            .GetMethod("Strip");
    }
    
    public void ProcessOneScript(string fileName)
    {
        string shortName = Path.GetFileNameWithoutExtension(fileName);
        if(shortName.ToLower().EndsWith(".debug"))
        {
            string debugScript = "";
            using(StreamReader sr = File.OpenText(fileName))
            {
                debugScript = sr.ReadToEnd();
            }
            
            string releaseScript = stripMethod
                .Invoke(null, new object[] { debugScript }).ToString();    
            
            string releaseScriptFileName = Path.Combine(
                Path.GetDirectoryName(fileName), 
                Regex.Replace(shortName, ".debug$", ".js")
            );
            
            using(StreamWriter sw = new StreamWriter(releaseScriptFileName))
            {
                sw.Write(releaseScript);
            }
        }
    }
    
    public void Process()
    {
        DirectoryInfo di = new DirectoryInfo(webRootPath);
        foreach (FileInfo fi in di.GetFiles(extension, SearchOption.AllDirectories))
        {
            ProcessOneScript(fi.FullName);
        }
    }
#>

Where "..\Auto_generating_strong_typed_navigation_class" should point to web application path relative to the folder of a template.

4) Click on "Run custom tool" context menu item of a template:

image

That's it !

 

Every time you click this menu item, the whole directory structure of web application will be scanned and every js file that ends with ".debug.js" will be converted to stripped version to use in release build.

The most important method here is ProcessOneScript - the method uses reflection to load and execute JavaScriptCommentStripper assembly for each file in the directory, then it just creates new (or rewrites existing) file with name of script modified by removing ".debug" in the end.

 

And I wanted to add - if you read my previous post - Auto generate strong typed navigation class for all user controls in ASP.NET web application, and use it with this scenario you can also generate strong typed representation of every JS file url:

image

To enumerate through .js files you need to modify the code I showed in previous article. I will place it here, and hope it helps:

<#@ import namespace="System.IO" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ template hostspecific="True" #> 
namespace Devarchive_Net.Navigation
{
<# 
string BaseDir = Host.ResolvePath(@"..\Auto_generating_strong_typed_navigation_class");
#>
<#=GetStrongTypedNav(BaseDir, "*.ascx", "Pages")#>
<#=GetStrongTypedNav(BaseDir, "*.js", "JsCsripts")#>
}

<#+
        public static string GetStrongTypedNav(string BaseDir, string extension, string rootClassName)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine(String.Format("\tpublic static class {0}", rootClassName));
            sb.AppendLine("\t{");
            DirectoryInfo di = new DirectoryInfo(BaseDir);
            GetClasses(di, sb, 2, "~/", extension);
            sb.AppendLine("\t}");
            return sb.ToString();
        }

        public static void GetClasses(DirectoryInfo di, StringBuilder sb, int ident, string curDir, string extension)
        {
            string identString = "";
            for (int i = 0; i < ident; i++) identString += "\t";
            if (di.GetFiles(extension, SearchOption.AllDirectories).Length > 0)
            {
                foreach (DirectoryInfo diCh in di.GetDirectories())
                {
                    if (diCh.GetFiles(extension, SearchOption.AllDirectories).Length > 0)
                    {
                        int nextIdent = ident + 1;
                        string nextDir = String.Concat(curDir, diCh.Name.ToLower(), "/");
                        sb.Append(identString);
                        sb.AppendLine(String.Format("public static class {0}", diCh.Name));
                        sb.Append(identString);
                        sb.AppendLine("{");
                        foreach (FileInfo fi in diCh.GetFiles(extension, SearchOption.TopDirectoryOnly))
                        {
                            sb.Append(identString);
                            bool NAV = true;
                            using (StreamReader sr = new StreamReader(fi.FullName))
                            {
                                while (!sr.EndOfStream)
                                {
                                    string line = sr.ReadLine();
                                    if (line.Contains("<%--NONAV--%>"))
                                    {
                                        NAV = false;
                                        break;
                                    }
                                }
                            }
                            if(NAV)
                            {
                                sb.AppendLine(String.Format("\tpublic const string {0} = \"{1}\";", Path.GetFileNameWithoutExtension(fi.Name).Replace(".", "_"), String.Concat(nextDir, fi.Name)));                            
                            }
                        }
                        GetClasses(diCh, sb, nextIdent, nextDir, extension);
                        sb.Append(identString);
                        sb.AppendLine("}");
                    }
                }
            }
        }
#>

In this scenario we can use the generated scripts and generated class to add references from c# code:

using System;
using System.Web.UI;
using Devarchive_Net.Navigation;

public partial class _Default : System.Web.UI.Page 
{
    protected void Page_Load(object sender, EventArgs e)
    {
        ScriptReference sr = new ScriptReference(JsScripts.Scripts.Script);
        sr.ScriptMode = ScriptMode.Inherit;
        SM.Scripts.Add(sr);
    }
}

Hope this helps.

Technorati Tags: ,

4 comments:

  1. thanks for this script.
    works well!

    ReplyDelete
  2. Good Day!!! blog.devarchive.net is one of the best innovative websites of its kind. I take advantage of reading it every day. Keep it that way.

    ReplyDelete
  3. It's a really good script!! keep on updating!!!

    ReplyDelete