Sunday, January 2, 2011

Apache Ant and Multi-file CSS / JavaScript Minification with YUI Compressor

Recently for my web site I found I needed to use a CSS / JavaScript compressor, as part of a strategy to decrease pageload times. The goals were:

  1. Keep the original CSS and JavaScript in my source tree readable.
  2. Convert about 15 or so CSS files (one for each neighborhood of the web site) and a half a dozen JavaScript files (versioned as 0.1, 0.2, 0.3, etc.) as part of an automated build.
  3. Deploy the compressed (or minified) versions of all the CSS and JavaScript files to staging and then to production.

Scanning what was out there, I settled on Yahoo's YUI Compressor, a decent fit because it's available as a Java program that I could run as part of my builds, which are currently based on Apache Ant (I'm in the process now of switching to Gradle, but that's a topic for another time).

So, on to the challenge.

I started with Adam Presley's blog entry, which details how to use YUI's Ant task to minify one CSS file and one JavaScript file, and generalized it to work on a set of CSS and JavaScript files. The other pre-requisite is the ant contrib library (to pick up the foreach task for iterating over a set of files).


Assumptions:

  1. CSS files are in a source directory (the property css.src.dir below), and are built (i.e. minified / compressed) into a second build directory (the property css.build.dir below), without changing the file name.
  2. Ditto for the JavaScript files (compress from scripts.src.dir to scripts.build.dir).

Here's a snippet of the code I came up with:


    <taskdef resource="net/sf/antcontrib/antcontrib.properties"/>
    <property environment="env"/>
    <property name="src.dir" value="${basedir}/src"/>
    <property name="scripts.src.dir" value="${src.dir}/scripts"/>
    <property name="css.src.dir" value="${src.dir}/css"/>
    <property name="compressorJar" value="${basedir}/lib/yuicompressor-2.4.2.jar"/>

    <!-- Build areas -->
    <property name="build.dir" value="${basedir}/bin"/>
    <property name="scripts.build.dir" value="${build.dir}/scripts"/>
    <property name="css.build.dir" value="${build.dir}/css"/>

    <!-- Staging directory to copy files -->
    <property name="static.deploy" value="${basedir}/../staging/static"/>

    <!-- Prepares the build directory -->
    <target name="prepare" >
        <mkdir dir="${build.dir}"/>
    <mkdir dir="${css.build.dir}"/>
    <mkdir dir="${scripts.build.dir}"/>
    </target>
    <target name="minifyCSS" depends="minifyCSSCheck" unless="no.minify">
        <echo message="minifying CSS: ${css.base.file}" />
        <java jar="${compressorJar}" fork="true" failonerror="true">
            <arg value="--line-break" />
            <arg value="4000" />
            <arg value="-o" />
            <arg value="${css.build.dir}/${css.base.file}" />
            <arg value="${css.src.dir}/${css.base.file}" />
        </java>
    </target>

    <target name="minifyCSSCheck">
        <basename property="css.base.file" file="${css.file}"/>
        <uptodate property="no.minify" srcfile="${css.src.dir}/${css.base.file}" targetfile="${css.build.dir}/${css.base.file}"/>
    </target>

    <target name="minifyJavaScript" depends="minifyJavaScriptCheck" unless="no.minify">
        <echo message="minifying JavaScript: ${script.base.file}" />
        <java jar="${compressorJar}" fork="true" failonerror="true">
            <arg value="--line-break" />
            <arg value="4000" />
            <arg value="--type" />
            <arg value="js" />
            <arg value="--preserve-semi" />
            <arg value="-o" />
            <arg value="${scripts.build.dir}/${script.base.file}" />
            <arg value="${scripts.src.dir}/${script.base.file}" />
        </java>
    </target>
    <target name="minifyJavaScriptCheck">
        <basename property="script.base.file" file="${script.file}"/>
        <uptodate property="no.minify" srcfile="${scripts.src.dir}/${script.base.file}" targetfile="${scripts.build.dir}/${script.base.file}"/>
    </target>

    <target name="minify">
        <foreach target="minifyCSS" param="css.file">
            <path>
                <fileset dir="${css.src.dir}" includes="**.css"/>
            </path>
        </foreach>
        <foreach target="minifyJavaScript" param="script.file">
            <path>
                 <fileset dir="${scripts.src.dir}" includes="**.js"/>
            </path>
        </foreach> 
    </target>




The main tasks are minifyCSS and minifyJavaScript, which work on one file at a time. Before invoking the YUI compressor, the uptodate task checks to see if the current compressed file's build time is not as recent as the source file.


Iteration over all of the files to be minified is taken care of by the minify task.

Note that this implementation still forks a JVM for each and every file that is minified.

2 comments: