Using Nant with Yahoo YUI JavaScript Compressor

After building the UI for a reasonably sized web application (which happens to be very heavy in javascript), I found that with around 40 JavaScript files being included into the page and the size of all of the files together caused for some poor load times on anything but a very fast internet connection. A common technique to solve issues such as this is to use a JavaScript compression technique such as js-min, YUI compressor, dojo compressor, and others. Because the YUI compressor gives very good results (better than the others in the tests I saw) and is very easy to use, I chose it. Besides doing the obvious, which is removing whitespace, the YUI compressor actually builds a parse tree and it thus able to replace local variables with shortened names. It is even safe enough to use with common gotchas such as eval and setTimeout.
To use the YUI Compressor you'll need to download the latest release and have Java installed. From there its as simple as running the command:

 java -jar yuicompressor-x.y.z.jar [options] [input file]

This is great, however this simply compresses one input file and writes it to standard out. What we want is to compress a group of files (thus eliminating the need for many requests).
To do this, I chose to use the Nant's concat contrib task.

Then I setup my build file to include a fileset of all of the javascript files that I wanted to include in my pages. Note that if you use different javascript files on different pages, you'll want to group your fileset such that you include some kind of core files in one set, then page specific groups in another set (if you even want to bother compressing the page specific ones to begin with). Since the website I'm building is pretty much all contained in a single page, I have all code that is specific to this project concatenated into one file I named core.js. Then all of the third party javascript files are concatenated into a file named third_party.js.

There is one major problem with the <concat> task that caused me much trouble (several hours of googling before coming up empty handed). The problem was that the concat task, which takes a fileset, does not concat the files in the order that you specify, even if you have no wildcard patterns. This didn't seem like the proper behaviour to me but there didn't seem to be a way around it. I would specify a specific order the files needed to be concatenated but then the actual data was in a completely different order (alphabetic).

Now an extremely ugly solution would have been to name the files in a way that the alphabetic ordering of them would produce the correct output. This didn't sound like a good idea and I hate resorting to ugly hacks like that.
I found several other build scripts online that used the concat task but if I remember correctly, they didnt use a normal fileset, they had a fileset for each file (or something like that, the syntax was ugly and extremely redundant, which turned me off).
I almost resorted to doing it this way, when I saw that there was an asis parameter that the include tag of a fileset could take. Turns out, the asis parameter will disable any kind of pattern matching, and the files will be concatenated in order. However, this will render the basedir property of the fileset useless. At this point our fileset looks something like this:

<fileset id="javascript-core-files">
  <include asis="true" name="relative/path/to/js_file.js" />
  <include asis="true" name="relative/path/to/js_another_file.js" />
  <include asis="true" name="relative/path/to/js_another_file_again.js" />
 ......
 </fileset>

Now this almost works. If you simply run this nant script by itself it works fine. However, our build system has a master build file that is run from another directory and will run tasks from this build file if it has a match for the input task name. When running the master build file the task which used this fileset would fail because the relative path was not computed from the current file, but from where the master build file was. The solution was to determine the path of the current file and put it in a variable, and use that variable instead of a hardcoded relative path.
Here is how I computed the current directory:

<property name="pwd" value="${project::get-base-directory()}" />

Note that on a windows system this will give you \ directory separaters (which may cause some cross platform compatibility issues. However, if you are building .Net apps, you are probably doing everything in windows which makes the point moot. Neverless, you can use another Nant function to replace the backslashes with forward slashes if you need to. Check the documenation for details.

At this point, our build script should have at least one fileset for the javascript files, a task for concatenating the files, and a task for compressing the files.
Here is an example of those three things:

 <property name="pwd" value="${project::get-base-directory()}" />
<property name="js_dir" value="${pwd}/relative/path/to/js" />
<fileset id="javascript-core-files">
  <include asis="true" name="${js_dir}/js_file.js" />
  <include asis="true" name="${js_dir}/js_another_file.js" />
  <include asis="true" name="${js_dir}/js_another_file_again.js" />
 ......
 </fileset>

<task name="concat-js">
 <concat destfile="${js_dir}/output/core.js">
    <fileset refid="javascript-core-files"/>
  </concat>
</task>

 <task name="yui-compress-js">
<exec program="java" 
    commandline="-jar  c:\path\to\yuicompressor.jar -o ${js_dir}/output/core.yuicomp.js ${js_dir}/output/core.js" />
</task>

The concat task takes all of the files in javascript-core-files and concatenates them into a single file which we'll put in a separate folder (named output) and call the file core.js. Then we'll use the yui-compress task to compress that file to output/core.yuicomp.js
I prefer keeping these auto generated files in another folder where I can remove them from SVN (avoiding conflicts).

Also, instead of using the exec's output property to specify the file to output to, I use the -o option of the yuicompressor. This is because Nant will write the output of any exec to both STDOUT and to the file you specify (if you use this parameter). By using the -o parameter, the yuicompressor does not output to standard out at all, and thus you won't have a huge blob of JavaScript output in your build output if you do it like I did.

One last thing, you can also compress your css files the same way which is just one more way to reduce bandwidth and load times for your users.

Update (2009-10-6)

Although my nant task worked great for doing the concat/compressing of javascript and CSS files, I ran into an issue when trying to develop the UI portion from my linux environment.
Since in our development environment we have an XML api that we use to build up our pages using AJAX, I can run a proxy that for a certain request will be made external, but pull everything else off a local filesystem. This allows me to develop wherever I want and test against our back end system. Unfortunately, since I had bundled this task into our NANT build script, I was unable to run it from linux (too many issues to bother, even if NANT is available in linux).
Instead what I did was create text files for each set of resources I wanted compressed, this ended up being 1 for core.js (javascript written specificaly for this application), 1 for third_party.js (all third party javascript/javascript libraries) and 1 for core.css. This is all I need for now.
In these files were a list of filenames of which contents I wanted combined into a single file.
I then wrote a windows shell script and a bash script to do the concat/compressing of these files..

Here are the following scripts (for your reference)

Windows

combine.bat
<code>
@echo off

rem  ============================================
rem  combine.bat
rem  usage: combine {file_of_filenames} {prefix_path} {output_file}
rem  will combine/concatenate all of the files that are in the {file_of_filenames} (one per line)
rem  where the filenames are specified relative to {prefix_path} which should end with a backslash - \
rem  and puts the output into {output_file}
rem
rem  Only works for Win2k+
rem
rem  ============================================


if a == a%3 goto USAGE

set path_to_files=%2
type nul > %3
setlocal enabledelayedexpansion
for /F "tokens=*" %%A in (%1) do (
  set file_path=%%A
  set file_path1=!file_path:/=\!	
  type %path_to_files%!file_path1! >> %3
)
endlocal
goto end

:USAGE
echo Usage: %0 {filenames_file} {base_file_path} {output_file}
exit /B 1

:END

then I call this script with another windows script that passes the parameters I need:

rem  ============================================
rem  do_combine.bat
rem  usage: do_combine
rem  script which utilizes combine.bat to do the 
rem  concatenation for each set of files we want
rem  compressed in our project, and then it 
rem  uses yui compressor to do the compression.
rem  Only works for Win2k.
rem  ============================================
@echo off
set OLDDIR=%CD%
rem cd to the current directory of the script (little trick i found online)
cd /d %0\..
set project_path=.\path\to\project\
set js_path=%project_path%js\
set js_third_party_path=%js_path%third_party\
set css_path=%project_path%css\
set output_path=%project_path%output\

call combine.bat core_js_files.txt %js_path% %output_path%core.js
call combine.bat third_party_js_files.txt %js_third_party_path% %output_path%third_party.js
call combine.bat css_files.txt %css_path% %output_path%core.css

java -jar yuicompressor-2.4.2.jar -o %output_path%core.min.js %output_path%core.js
java -jar yuicompressor-2.4.2.jar -o %output_path%third_party.min.js %output_path%third_party.js
java -jar yuicompressor-2.4.2.jar -o %output_path%core.min.css %output_path%core.css
 
rem change back to the original directory
chdir /d %OLDDIR%

The linux equivalent of the two above files is this:

#!/bin/bash
project_path=./path/to/project/
files=(core_js_files.txt third_party_js_files.txt css_files.txt)
output_files=(core.js third_party.js core.css)
input_paths=(${project_path}js/ ${project_path}js/third_party/ ${project_path}css/)
output_path=${project_path}output/
mkdir -p ${output_path}
end=$((${#files[@]} - 1))
for i in $(seq 0 $end)
do
  #grab the contents of the file
  contents=$(cat ${files[i]} | tr -d "\r")
  output_file=${output_path}${output_files[i]}
  #clear the output file
  :>${output_file}
  for j in $contents
  do
    input_file=${input_paths[i]}${j}
    cat ${input_file} >> ${output_file}
  done
  min_basename=${output_file%.*}
  min_ext=${output_file##*.}
  min_fullname=${min_basename}.min.${min_ext}
  java -jar yuicompressor-2.4.2.jar -o ${min_fullname} ${output_file}
done

Perfect

Great post: clear, to the point, and best of all it works like a charm! Thanks very much :)