Custom NAnt JSMin Task For Minifying JavaScript Files

September 19, 2008

We recently created an automated build script for Ra-Ajax using NAnt. We of course needed to minify the JavaScript files as part of this build process. The most widely used tool for this specific purpose is Douglas Crockford's JSMin and we decided to use it.

You can download this custom NAnt task from here. This zip file includes the code for this task, a pre-built dll and our NAnt build script for reference purpose on how to use the task.

Naturally because we try as hard as we can to be platform neutral and support both Microsoft's ASP.NET and Mono's ASP.NET; the build script needed to work on both Windows and Linux. So calling the executable provided by Mr. Crockford from the NAnt script, is not our best option.

We wanted to specify, in the NAnt script, a fileset of the JavaScript files that we want to minify and a target directory which will contain all the resulting minified files from JSMin, if we went down the road of calling an external executable from the NAnt script, achieving the abovementioned goal would probably have made the NAnt script longer than it should be and it would probably lose simplicity.

The next step was to try and find a Custom JSMin NAnt Task, built by someone who probably faced this problem before. Having a JSMin NAnt Task would make the script much simpler and easier to understand and modify by anyone. Although we expected otherwise, we were not able to find one. So we decided to build it and make it available for anyone facing the same situation.

How You Can Use it


The project, Ra.Build.Tasks which is basically this custom JSMin Task, is available here as part of the Ra-Ajax source code. If you compile this project you will get a Ra.Build.Tasks.dll, drop that dll in NAnt's bin folder and you can use it immediately in your NAnt build script like this:


<jsmin verbose="true" todir="Ra/JsCompressed">
<fileset basedir="Ra/Js">
<include name="/**/*.js" />
</fileset>
</jsmin>


You can also, instead of copying the Ra.Build.Tasks.dll to NAnt's bin folder, use the "loadtasks" task in your NAnt script, like this:

<loadtasks assembly="Ra.Build.Tasks.dll" />

Let's go over the important parts that we need to note here. The above task basically means take all the JavaScript files under the 'Ra/Js' directory, and its sub directories, minify them and put the resulting minified JavaScript files under the 'Ra/JsCompressed' directory. Note also that we specify verbose output; this will make NAnt log to the console window the path of each file being minified and the path of its corresponding minified file, like this:

[jsmin] Minifying 3 JavaScript file(s) to 'C:\ra-ajax\Ra\JsCompressed\'.
[jsmin] Minifying 'C:\ra-ajax\Ra\Js\JsCore\Ra.js' to 'C:\ra-ajax\Ra\JsCompressed\JsCore\Ra.js'.
[jsmin] Minifying 'C:\ra-ajax\Ra\Js\Behaviors.js' to 'C:\ra-ajax\Ra\JsCompressed\Behaviors.js'.
[jsmin] Minifying 'C:\ra-ajax\Ra\Js\Control.js' to 'C:\ra-ajax\Ra\JsCompressed\Control.js'.

If verbose is set to false or not used at all, NAnt will just report the first line which includes the number of files being minified and the target directory that the resultant minified files will be in after minification.

Another thing to note is that the directory structure under the source directory of the original un-minified JavaScript files will be replicated to the target directory. This is of particular importance in our case, since when we compile the library and embed the minified JavaScript files as resources, the resource names will be taken care of automatically. Note that the files Behaviors.js and Control.js were directly under the 'Ra/Js' directory, consequently their corresponding minified files were placed directly under the target directory 'Ra/JsCompressed', while the file Ra.js was under another sub directory called 'JsCore' and this was reflected to the target directory.

Although this is the default behavior and you will most likely want the task to do this, you can override it by simply using the task attribute called flatten, like this:

<jsmin flatten="true" ...>...</jsmin>

This will make the task ignore the source directory structure and will cause the minified JavaScript files to be placed directly under the specified target directory.

One final thing you need to notice is that on the fileset you must specify the basedir, which is basically the source directory; otherwise the task will not be able to correctly replicate the source directory structure to the target directory. So you always must write the fileset like this:

<fileset basedir="Ra/Js">
<include name="/**/*.js" />
</fileset>

And not like this:

<fileset>
<include name="Ra/Js/**/*.js" />
</fileset>

This task was tested on Windows and Linux (Ubuntu) and worked as expected on both. You are free to use this task as you wish. You can use it in open source or commercial applications, you can modify it, republish it and basically do whatever you want with it as long as it is for good not evil and that you include the respective copyright notices. If you are interested in how this custom NAnt Task was built, please read on.

How We Built This Custom Task


You would probably start with creating a Class Library project, but one thing you need to know at the beginning is that if you plan to put this assembly in NAnt's bin folder, the name of the assembly should end with "Tasks" in order for NAnt to be able to detect it and load the tasks inside that assembly. However, if you plan to use the "loadtasks" task to load the tasks from the assembly in your NAnt script, the name of the assembly does not need to end with "Tasks".

The next step is to reference NAnt.Core.dll and add a C# class file called JsMinTask.cs to the project. For the minification logic, we used Douglas Crockford's C# class "jsmin.cs", provided here. Mr. Crockford generously provided the minification logic, in several languages, freely for anyone to use.

Now back to our JsMinTask class, we need to inherit from NAnt.Core.Task and decorate the class with the TaskName attribute to provide the corresponding name of the task that will be used in the NAnt script.

namespace Ra.Build.Tasks
{
[TaskName("jsmin")]
public class JsMinTask : NAnt.Core.Task
{
...
}
}

We will have three properties in this class, each will typically correspond to an attribute that we specify on the task in the NAnt script, they are:

[TaskAttribute("todir", Required = true)]
public virtual DirectoryInfo ToDirectory
{
get { return _toDirectory; }
set { _toDirectory = value; }
}

[TaskAttribute("flatten")]
[BooleanValidator()]
public virtual bool Flatten
{
get { return _flatten; }
set { _flatten = value; }
}

[BuildElement("fileset", Required = true)]
public virtual FileSet JsFiles
{
get { return _jsFiles; }
set { _jsFiles = value; }
}

You can see that Flatten and ToDirectory are decorated with the TaskAttribute to specify the attribute name in the NAnt script that will correspond to this property. You will also see that ToDirectory is marked as required attribute, so it can't be omitted when you use the jsmin task, otherwise you will get an error when you run your NAnt build script. The flatten attribute however is optional and can be omitted; in such case its value will be false.

The property JsFiles however is a little bit different, it is of type NAnt.Core.Types.FileSet and it represents the fileset that we use to indicate the JavaScript files we need to minify in the NAnt script, like this:

<fileset basedir="Ra/Js">
<include name="/**/*.js" />
</fileset>

Note that we decorate this property with the BuildElement attribute since it is not just a simple attribute on the Task but rather a nested XML element, we specify its corresponding name in the NAnt script as well "fileset" and we also indicate that it is required, not optional, so the user of the task must specify it.

Now to the exciting parts, we override two methods form our base class here, InitializeTask and ExecuteTask. Their names are fairly descriptive and they do exactly what their names suggest. In the InitializeTask, we add some initialization code to make sure that the variables _toDirecotry and _jsFiles are not null, otherwise we throw a NAnt.Core.BuildException, with appropriate message, to indicate to the user that something is wrong. We also create the target directory here if it does not exist.

protected override void InitializeTask(XmlNode taskNode)
{
if (_toDirectory == null)
throw new BuildException(
string.Format(CultureInfo.InvariantCulture, "The 'todir' attribute must be set ..."),
Location
);

if (_jsFiles == null)
throw new BuildException(
string.Format(CultureInfo.InvariantCulture, "The <fileset> element must be used ..."),
Location
);

if (!_toDirectory.Exists)
_toDirectory.Create();
}

And in ExecuteTask we have our main logic:

protected override void ExecuteTask()
{
if (_jsFiles.BaseDirectory == null)
_jsFiles.BaseDirectory = new DirectoryInfo(Project.BaseDirectory);

Log(Level.Info, "Minifying {0} JavaScript file(s) to '{1}'.",
_jsFiles.FileNames.Count, _toDirectory.FullName);

foreach (string srcPath in _jsFiles.FileNames)
{
FileInfo srcFile = new FileInfo(srcPath);

if (srcFile.Exists)
{
string destPath = GetDestPath(_jsFiles.BaseDirectory, srcFile);

DirectoryInfo destDir = new DirectoryInfo(Path.GetDirectoryName(destPath));

if (!destDir.Exists)
destDir.Create();

Log(Level.Verbose, "Minifying '{0}' to '{1}'.", srcPath, destPath);

new JavaScriptMinifier().Minify(srcPath, destPath);
}
else
{
throw new BuildException(
string.Format(
CultureInfo.InvariantCulture,
"Could not find file '{0}' to minify.",
srcFile.FullName),
Location
);
}
}
}

We first check to see if BaseDirectory property of _jsFiles is null, i.e. "basedir" attribute was not specified on the fileset, and if it is, we set it to the Project's BaseDirectory which is typically the directory that contains your NAnt build file. Then we log the main message of the task, we log this message at the Info level, which is the default level for logging, so it will be displayed at the NAnt output console window regardless if we have specified verbose output or not.

Next, we loop over the file paths of the original un-minified JavaScript files, _jsFiles.FileNames, and we check if this specific file exists or not, if it does not, we throw a BuildException to inform the user of that fact. If it exists however, we determine the path of the corresponding minified JavaScript file using a little helper method "GetDestPath". Then we check if the parent directory of that estimated file path is present or not, if it is not we create it.

After that we log a message, at the verbose level this time, so it will only be displayed if the user specifies verbose="true" on the task. This message is the individual log message for each minified file that looks like this:

[jsmin] Minifying 'C:\ra-ajax\Ra\Js\Behaviors.js' to 'C:\ra-ajax\Ra\JsCompressed\Behaviors.js'.

Finally, we call the Minify method of the JavaScriptMinifier object and pass to it, the path of the un-minified JavaScript file and the estimated path of the minified JavaScript file and let it do its work.

We hope you will find this custom NAnt Task useful in your project. Again, you can find the code here as part of the Ra-Ajax source code. You are free to use, modify, republish as you wish as long as you conform to the respective license conditions provided at the top of the source code files of that project "Ra. Build.Tasks". There is always of course room for improvements and additions, if you make any, you can contact me or Thomas and if we find the modifications relevant, we will be happy to add them and give you credit.



Ra-Ajax 0.5.3 Released - Next >>