Chapter 16. Build Tools, Testing, and Production Considerations

After all your hard work developing with Dojo, there comes a point when your application is ready for prime time. Util provides terrific build tools and a testing framework that can get you ready for production before you know it. The build tools provided by Util are the same ones that are used to produce each official Dojo release, and the Dojo Objective Harness (DOH) is a unit-testing framework that facilitates achieving some automated quality assurance before your app ever gets out the door.

Building

For any production setting, minimizing the overall footprint of your JavaScript files and the number of synchronous requests to the server is absolutely essential. The difference in downloading scores of individual resource files via synchronous requests incurred by dojo.require versus one or two calls back to the server makes all the difference in the world in terms of a snappy page load.

Dojo's build tools makes accomplishing what may initially seem like such an arduous task quite easy. In a nutshell, the build tools automate the following tasks:

  • Consolidates multiple modules into a single JavaScript file called a layer

  • Interns template strings into JavaScript files, including layers, so that a standalone template is no longer needed

  • Applies ShrinkSafe, a JavaScript compressor based on Rhino, to minify the size of the layers by removing whitespace, linebreaks, comments, and shortening variable names

  • Copies all of the "built" files into a standalone directory that can be copied and deployed to a web server

One reason you may not have been aware of the build tools is that they aren't included in the util directory of an official release. To get them, you have to download a source release (a source release will have the -src suffix on the file base part of the filename) or just grab the source from the Subversion trunk. Chapter 1 provides an overview of getting the Dojo from Subversion, but basically, all that is necessary is to point your client at the Dojo repository and wait for it to download everything, whether it is the trunk or a specific tag.

In either case, you'll find that the util directory now holds some additional directories; one of these directories is buildscripts, which contains the goods we're looking for.

Tip

http://svnbook.red-bean.com/ contains the unofficial Subversion book, which is available in a variety of formats. Taking a moment to bookmark this valuable resource now will save you time later.

To run the build tools, you'll have to have Java 1.4.2 or later installed, available from http://java.sun.com (because ShrinkSafe is based on Rhino, which is written in Java). But don't worry about having to be a Java programmer to use ShrinkSafe; ShrinkSafe comes packaged as a single jar file (an executable Java archive), so you can treat it like any other executable.

Running a Build

The primary entry point for kicking off a build is via the buildscripts/build.sh (or build.bat for Windows users), and is really just a call through to the custom Rhino jar that does all of the work based on a custom profile that is provided (more on that in just a moment). As an ordinary executable, however, build tools such as Make or ant can easily include the jar file as an ordinary part of the production build process. This ability is especially convenient when server-side components are based on languages that must be compiled.

Executing the corresponding build script or executing the jar without any command-line options provides an impressive list of options. Table 16-1 is adapted directly from the standard option list that is displayed.

Table 16-1. Build script parameters

Option

Description

xdScopeArgs

If the loader=xdomain build option is used, then the value of this option will be used as the arguments to the function that defines the modules in the .xd.js files. This allows for more than one version of the same module to be in a page. See documentation on djConfig.scopeMap for more information.

cssOptimize

Specifies how to optimize CSS files. If comments is specified, then code comments and line returns are stripped. If comments.keepLines is specified, then code comments are stripped, but line returns are preserved. In either case, @import statements are inlined.

releaseName

The name of the release. A directory inside releaseDir will be created with this name. By default, this value is dojo.

localeList

The set of locales to use when flattening i18n bundles. By default this value is cs,de-de, en-gb,en-us,es-es,fr-fr,hu,it-it,ja-jp,ko-kr,pl,pt-br,ru, zh-tw,zh-cn.

releaseDir

The top-level release directory where builds end up. The releaseName directories will be placed inside this directory. By default, this value is ../../release/.

copyTests

Turn on or off copying of test files. This value is true by default.

symbol

Inserts function symbols as global references so that anonymous functions will show up in all debuggers (especially in IE, which does not attempt to infer function names from the context of their definition). Valid values are long and short. If short is used, then a symboltables.txt file will be generated in each module prefix's release directory that maps the short symbol names to more descriptive names.

action

The build action(s) to run. Can be a comma-separated list, like action=clean,release. The possible build actions are: clean and release. This value is help by default.

internStrings

Turn on or off for widget template file interning. This value is true by default.

scopeMap

Change the default dojo, dijit, and dojox scope names to something else. Useful if you want to use Dojo as part of a JS library, but want to make a self-contained library with no external dojo/dijit/dojox references. Format is a string that contains no spaces, and is similar to the djConfig.scopeMap value (note that the backslashes below are required to avoid shell escaping):

scopeMap: [[\"dojo\",\"mydojo\"],[\"dijit\",\"mydijit\"],
  [\"dojox\",\"mydojox\"]]

optimize

Specifies how to optimize module files. If comments is specified, code comments are stripped. If shrinksafe is specified, the Dojo compressor is used on the files, and line returns is removed. If shrinksafe.keepLines is specified, the Dojo compressor is used on the files, and line returns are preserved. If packer is specified, Dean Edwards's Packer is used (see http://dean.edwards.name/packer/).

loader

The type of dojo loader to use. default (the default value) or xdomain are acceptable values.

log

Sets the logging verbosity. See util/buildtools/jslib/logger.js for possible integer values. The default value is 0.

profileFile

A file path to the profile file. Use this if your profile is outside of the profiles directory. Do not specify the profile build option if you use profileFile.

xdDojoPath

If the loader=xdomain build option is used, then the value of this option will be used to call dojo.registerModulePath( ) for dojo, dijit., and dojox. The xdDojoPath should be the directory that contains the dojo, dijit, and dojox directories, and it should not end in a slash. For instance: http://www.example.com/path/to/dojo.

version

The build will be stamped with this version string. The default value is 0.0.0.dev.

profile

The name of the profile to use for the build. It must be the first part of the profile file name in the profiles/ directory. For instance, to use base.profile.js, specify profile=base (the default).

layerOptimize

Specifies how to optimize the layer files. If comments is specified, code comments are stripped. If shrinksafe is specified, the Dojo compressor is used on the files, and line returns are removed. If shrinksafe.keepLines is specified, the Dojo compressor is used on the layer files, and line returns are preserved. If packer is specified, the Dean Edwards's Packer is used. shrinksafe is the default.

xdDojoScopeName

If the loader=xdomain build option is employed, the value of this option is used instead of dojo (the default) for the dojo._xdResourceLoaded( ) calls that are done in the .xd.js files. This allows for dojo to be under a different scope name, but still allows XDomain loading with that scope name.

cssImportIgnore

You can use cssOptimize=comments to force the @import inlining process to ignore a set of files. The value of this option should be a comma-separated list of CSS filenames to ignore. The filenames should match the string values that are used for the @import calls.

buildLayers

A comma-separated list of layer names to build. Using this option means that only those layers will be built. This helps if you are doing quick development and test cycles with layers. If you have problems with this option, try removing it and doing a full build with action=clean,release. This build option assumes you have done at least one full build first.

symbol

Inserts function symbols as global references so that anonymous functions will show up in all debuggers (especially IE, which does not attempt to infer function names from the context of their definition). Valid values are long and short. If short is used, then a symboltables.txt file will be generated in each module prefix's release directory, mapping the short symbol names to more descriptive names.

scopeDjConfig

Burns a djConfig object into the built dojo.js file, which is useful if you are making your own scoped build and you want a djConfig object local to your version that will not be affected by any globally declared djConfig object in the page. This value must be a string that will look like a JavaScript object literal once it is placed in the built source. Can also be useful for situations where you want to use Dojo as part of a JavaScript library that is self-contained and has no external dojo, dijit, or dojox. Example:

scopeDjConfig={isDebug:true,scopeMap:[[\"dojo\",\"mydojo\"],
  [\"dijit\",\"mydijit\"], [\"dojox\",\"mydojox\"]]}

Note that the backslashes are required to avoid shell escaping if you type this on the command line.

While all of those options may seem like a lot to manage, the routine builds are really quite simple and involve only a handful of options. But first, we need a profile.

Build Profiles

A profile is the configuration for your build as provided via the profile or profileFile option. The most basic function of a profile is to specify the exact Dojo resources that should consolidated into a standalone JavaScript file, also known as a layer; a typical rule of thumb is that each page of your application should have its own layer. The beauty of a layer is that it is an ordinary JavaScript file, and can be included directly into the head of a page, loading everything you've crammed into it via a single synchronous request to the server—well, sort of. By convention, Base is so heavily used that it generally stays in its own individual dojo.js file, so you normally have two synchronous calls, one for Base, and one for your own layer.

Setting up a build profile

Assuming your application has three distinct pages, you might have three layer files and one copy of Base.

Tip

If you really want to bundle up your own modules inside of the dojo.js file that normally only contains Base, you can name your layer dojo.js. However, it's often a good idea to keep Base separated because it would be used in every page of you application and is cacheable by your web browser.

Physically speaking, a profile is simply a file containing a JSON object. Example 16-1 shows a profile that consolidates several of the form dijits that are explicitly dojo.required into a page. All internal dependencies are tracked down automatically. Just like with dojo.require, you state what you need to use directly, and dependency tracking is automated behind the scenes for you.

Example 16-1. A simple build profile

dependencies ={
    layers:  [
        {
            name: "form.js",
            dependencies: [
                "dijit.form.Button",
                "dijit.form.Form",
                "dijit.form.ValidationTextBox"
            ]
        }
    ],
    prefixes: [
        [ "dijit", "../dijit" ]
    ]
};

Assuming the previous profile is located at util/buildscripts/profiles/form.profile.js and you're working in a Bash shell, the following command from within the util/buildscripts directory would kick off a build. Note that the profile option expects profiles to be of the form <profile name>.profile.js and only expects the <profile name> as an option:

bash build.sh profile=form action=release

Tip

If you don't want to save the file in util/buildscripts/profiles/form.profile.js, you can use the profileFile option instead of the profile option.

After executing the command, you should see a bunch of output indicating that the build is taking place and that of strings are being interned from template files into JavaScript files. The artifact of the build is a release directory containing dojo, dijit, and util. Inside of the dojo directory, you'll find the usual suspects, but there are four especially important artifacts to note:

  • The compressed and uncompressed version of Base, dojo.js and dojo.js.uncompressed.js

  • The compressed and uncompressed version of your form layer in form.js and form.js.uncompressed.js (go ahead and take a peek inside to see for yourself)

But what if you need resources that are not included in your custom layer file? No problem—if resources aren't included in a profile, they are fetched from the server whenever the dojo.require statement that specifies them is encountered. Assuming you take the entire release directory and drop it somewhere out on your server, the dojo.require statements requesting nonlayered resources will behave normally, though you will incur a small roundtrip cost for the request to the server.

Requests for Base functions and resources in your layer do not incur server-side requests when they are encountered in a dojo.require statement because they're already available locally. Resources not in your layer, however, incur the routine overhead of synchronous HTTP requests (Figure 16-1).

Conceptual server request illustrating various JavaScript files loading

Figure 16-1. Conceptual server request illustrating various JavaScript files loading

While you may generally want to include every possible resource that is needed in a build, there may be some situations where you want to lazy load. The tradeoff is always between a "small enough" initial payload size over the wire versus the cost of synchronous loading via dojo.require later.

Warning

If you accidentally misspell or otherwise provide a dependency that does not exist, ShrinkSafe may still complete your build even though it could not find all of the dependencies. For example, if you accidentally specify dijit.Button (instead of dijit.form.Button), you'll most likely still get a successful build, and you may not ever notice that dijit.form.Button wasn't bundled because a call to dojo.require("dijit.form.Button") would fetch it from the server and your application would behave as normal.

It's always a good idea to double-check your build by taking a look at the Net tab in Firebug to ensure that everything you expect to be bundled up is indeed bundled up.

Setting up a (more clever) build profile

A slightly more clever way to set up the build profile just discussed is to create a custom module that does nothing more than require in all of the resources that were previously placed in the layer via the profile file. Then, in the profile file, simply include the custom module as your sole dependency for the layer.

First, Example 16-2 shows how your custom module would look. Let's assume the module is dtdg.page1 and is located at called dtdg/page1.js.

Example 16-2. A custom module for a more clever build profile

dojo.provide("dtdg.page1");

dojo.require("dijit.form.Form");
dojo.require("dijit.form.Button");
dojo.require("dijit.form.ValidationTextBox");

Now, your profile need only point to the custom module, as the other dependencies are specified inside of it and will be tracked down automatically. Example 16-3 demonstrates an updated profile, which assumes your custom module directory is a sibling directory of util.

Example 16-3. Updated build profile

dependencies ={
    layers:  [
        {
            name: "form.js",
            dependencies: [
                "custom.page1"
            ]
        }
    ],
    prefixes: [
        [ "custom", "../custom" ]
    ]
};

Finally, your page might contain the following SCRIPT tag to pull in the module along with Base:

<script type="text/javascript"
djConfig="baseUrl: './',modulePaths: {custom:'path/to/custom/page1.js'},
    require: ['custom.page1']"
src="scripts/dojo.js"></script>

Standard build profile

Notice that the util/buildscripts/profiles directory contains a number of example build profiles as well as the standard.profile.js file that contains the layers for a standard build of Dojo. The standard profile builds Base as well as a baseline Dijit layer that contains common machinery that is used in virtually any circumstance involving dijits, as well as a couple of other useful layers. Note that any profile in the standard.profile.js file should be available over AOL's CDN. For example, to retrieve the baseline Dijit profile, you could simply execute the following statement:

dojo.require("dijit.dijit");

Remember, however, that the first SCRIPT tag should always be the one for Base (dojo.xd.js), so you'd include any additional SCRIPT tags for layers after the one for Base.

ShrinkSafe optimization and other common options

In virtually any production setting, you'll want to apply ShrinkSafe to minify all of your code. While the previous build example build did optimize the build in the sense that it minified dojo.js and form.js as well as interned template strings, ShrinkSafe can minify every file in the release.

Recall that the size "over the wire" is what really matters when you're talking about performance from a payload perspective. While files may be a set size as they exist on the server, most servers are able to apply gzip compression to them if the web browser is capable of handling it. While ShrinkSafe minifies JavaScript files by removing artifacts like whitespace, comments, and so on, the further compression is possible because the repetitive use of public symbols such as dojo, dijit, and your own custom tokens allows for actual compression to occur.

Tip

Minification is the reduction of a file's size by removing artifacts such as commas, whitespace, linebreaks, etc. Compression is an algorithmic manipulation that reduces a file's size by using by finding multiple instances of the same tokens and encoding an equivalent file by using shorter placeholders for the repetitive tokens. To learn more, see http://en.wikipedia.org/wiki/Gzip for an overview of gzip compression.

An especially notable feature of ShrinkSafe is that it never mangles a public API; this is a direct contrast to some JavaScript tools that attempt to encrypt JavaScript by applying regular expressions or convoluted logic to "protect" the script. In general, attempting to protect your JavaScript is mostly pointless. As an interpreted language that runs in the browser, the user of your application will almost certainly have access to your source code, and it's not terribly difficult to use a debugger to unroll the protected script into something that's fairly intelligible.

Tip

ShrinkSafe itself is not a Dojo-specific tool; you can apply it to any JavaScript file to gain the benefits of compression using the online demonstration at http://shrinksafe.dojotoolkit.org/. OS X users can download a version at http://dojotoolkit.org/downloads, and users of other platforms can grab the standalone custom Rhino jar from http://svn.dojotoolkit.org/dojo/trunk/buildscripts/lib/custom_rhino.jar.

In other words, ShrinkSafe shrinks your files without changing public symbol names. In fact, if you look at the form.js file that is an artifact of the previous build examples, you can see for yourself that ShrinkSafe strips comments, collapses and/or eliminates frivolous whitespace, including newline characters, and replaces nonpublic symbols with shorter names. Note that replacing all symbols with shorter, meaningless names qualifies as a lame attempt at encryption—not particularly useful for debugging purposes either.

Let's update our existing profile:

  • Minify all files in the release with the optimize="shrinksafe" option

  • Designate a custom notice that should appear at the top of every minified JavaScript file in an additional (mythical) foo module provided by CUSTOM_FILE_NOTICE.txt

  • Designate a custom notice that should appear at the top of the final form.js provided by the same CUSTOM_LAYER_NOTICE.txt

  • Provide a custom name for the release directory via the releaseName="form" option

  • Provide a custom version number for the build via the version="0.1.0." option

Here's the modified form.profile.js file from Example 16-1. Note that the information in the custom notices must be wrapped in JavaScript comments; the path for the custom notices should be relative to the util/buildscripts directory or an absolute path:

dependencies ={
    layers:  [
        {
            copyrightFile : "CUSTOM_LAYER_NOTICE.txt",
            name: "form.js",
            dependencies: [
                "dijit.form.Button",
                "dijit.form.Form",
                "dijit.form.ValidationTextBox"
            ]
        }
    ],
    prefixes: [
        [ "dijit", "../dijit" ],
        [ "foo", "../foo", "CUSTOM_FILE_NOTICE.txt" ]

    ]
};

The augmented command to kick off this build is straightforward enough, and creates the artifacts in the release/form directory that exist alongside the dojo source directories:

bash build.sh profile=form action=release optimize=shrinksafe releaseName=form
version=0.1.0

To actually use your custom release, simply include the paths to the compressed dojo.js and form.js files in script tags in the head of your page, like so. The dojo.js layer must be included first, because form.js depends on it:

<html>
  <head><title>Fun With Forms!</title>
    <!--  include stylesheets, etc. -->
    <script type="text/javascript" path="relative/path/to/form/dojo.js"></script>
    <script type="text/javascript" path="relative/path/to/form/form.js"></script>
  </head>
  <!--  rest of your page -->

And that's it. It takes only two synchronous requests to load the JavaScript (which now have interned templates) into the page; other resources included in your build via the prefixes list are at your disposal via the standard dojo.require statements.

If you are completely sure you'll never need any additional JavaScript resources beyond dojo.js and your layer files, it is possible to pluck out just the individual resources you need from the release directory structure. However, you'll have to go through a little extra work to track down dependencies with built-in CSS themes such as tundra because some of the stylesheets may use relative paths and relative URLs in import statements.

Warning

Inspecting the Net tab of Firebug is very useful in tracking down the dependencies you need to pluck out of the release directory, but be advised that Firebug may not display 404 (Not Found) errors for import statements that are used in stylesheets.

Get Dojo: The Definitive Guide now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.