Creating Builds

Dojo’s build system helps you create highly optimized resources by concatenating and minimizing JavaScript and CSS files. The build system is able to leverage the dependency declarations within JavaScript source files and @import declarations in CSS files to create these builds with minimal effort and maintenance.

  • Difficulty: Advanced
  • Dojo Version: 1.7

Why build?

Dojo’s modular programming model lends itself to small, well-organised blocks of encapsulated JavaScript (rather than large, monolithic scripts). However, used alone, this modular approach to development significantly increases page load times by adding tens to hundreds of extra HTTP requests to a session.

The Dojo build system solves this problem by generating layers that incorporate all the modules for an application into one or a handful of files automatically. As a result, we can create high-performance, single-file applications for deployment while still enjoying all the benefits of modular code development. In fact, the build system not only combines and compresses JavaScript files, but also inlines strings (like HTML templates) that use the dojo/text plugin, concatenates and compresses @imported CSS files, and can be extended with new functionality (such as code linting) by adding new transform plugins.

The build system in Dojo 1.7 has been completely rewritten from scratch. While many options look and work the same, and it is capable of reading most 1.6 build profiles, there are significant architectural changes—so don’t assume that your knowledge of the 1.6 build system will carry over cleanly. Also, if you are not familiar with the new AMD format in Dojo 1.7, make sure you read the Defining Modules tutorial first.

Basic requirements

To use the Dojo build system, you must have a copy of the full Dojo SDK. The “standard release” of Dojo is pre-built and does not include the build tool. The build tool itself relies on Java (and, optionally, Node.js for even faster builds), so make sure that have you that installed as well.

If you want to avoid the hassle of trying to set up your own builds from scratch, the Dojo Boilerplate includes everything you need to get up and running immediately with the Dojo build system.

Getting started

In order to create a build with the Dojo 1.7 build system, two files must exist inside your main application package directory. The first file is a CommonJS Packages/1.0 package descriptor, which is always named package.json and is placed in the root of the package directory. The second file is a build profile, which contains information about how the build tool should process the package’s contents. By convention, this file is typically named <package name>.profile.js and is also usually placed in the root of the package directory.

Typically, you’ll have just one package containing your entire application, but if you’ve split your code into multiple packages (for instance, with shared/common modules in a different package), you will need to create both of these files for each package.

The package descriptor

The package descriptor file provides information about the current package, such as the name of the package, information about its package dependencies, links to licensing and bug tracking information, and so on. For the purposes of the Dojo build system, the only really important key is the special key dojoBuild, though it is usually good to provide at least a name, version, and description. The dojoBuild key is used to point to the build profile file for the package. Given the example of a package named “app”, a bare-bones package descriptor would look like this:

{
	"name": "app",
	"description": "My application.",
	"version": 1.0,
	"dojoBuild": "app.profile.js"
}

The CommonJS Packages/1.0 specification provides a full list of possible options for the package descriptor.

The CommonJS specification lists several additional required fields that are not used by the build system; if you plan on uploading your code to a package repository, make sure you follow the specification and include all required fields.

The build profile

The build profile is the main configuration file for the build system. It is a JavaScript file that places all of the directives that are necessary to create a fully functional build on a profile object. The most basic build profile for a package looks like this:

var profile = {
	resourceTags: {
		amd: function(filename, mid) {
			return /\.js$/.test(filename);
		}
	}
};

The build profile linked to by dojoBuild only needs to contain a resourceTags directive if it’s not the primary build profile for the application itself.

This example profile simply tags all the JavaScript files in the package as being AMD modules, so that when the build system processes them, it does not accidentally assume a module is actually a legacy Dojo module and incorrectly apply legacy wrapper code. While such a basic profile could be useful for a package that only contains components (like the dojo and dijit packages), for a complete application, you’ll need to add some additional profile options:

  • releaseDir – the root directory where the built version of the application should go.
  • basePath – the root directory for the entire application’s JavaScript code, as a file system path.
  • action – the action to perform during the build. As of Dojo 1.7, this should always simply be "release".
  • layers – a list of “layer” files to build.
  • resourceTags – a list of functions that provide hints to the build system about how a given module should be treated by the build system.

You’ll probably also want to use some or all of these options, which make your builds smaller:

  • mini – enables a “mini” build, which excludes tests, demos, and original template files from the built version. Defaults to false.
  • optimize – enables the compression of individual JavaScript files. Defaults to no compression.
  • layerOptimize – enables the compression of concatenated layer JavaScript files. Defaults to using ShrinkSafe.
  • cssOptimize – enables compression of CSS files. Defaults to no compression.
  • stripConsole – strips calls to console functions (console.log, console.warn, etc.) in the built output. Defaults to no stripping.
  • selectorEngine – the default selector engine code to add to the built dojo.js file. Defaults to no selector engine.
  • staticHasFeatures – allows the build system to optimize out code branches that are not wanted. See the buildControlDefault.js file for the list of default has flags.

We’ll go over some of these options in greater detail.

Resource tags

So far, we’ve seen resources being tagged as AMD resources, but the build system handles more than just AMD modules. What about files that need to be copied as-is, files that should be excluded entirely from a mini build, or files that are actually legacy Dojo modules? Resource tags are the solution. While there are a few different resource tags that the build system understands, other than amd, the two most useful resource tags are copyOnly, which specifies files that should be copied as-is with no modification, and miniExclude, which specifies files that should not be copied to the built version of the application when mini is true. (test is another commonly used flag and tags files as being tests. This flag is used by the copyTests build option. With miniExclude, this is not terribly useful, so you probably won’t use it yourself.)

During the build process, resource tag functions are called with the filename and module ID for each file being processed. The function is then responsible for returning a true or false value corresponding to whether or not the given module should be tagged. In the previous example, all files with filenames ending in .js were tagged as being AMD resources. If we were to have some modules that should not be included in a mini build, we could create a simple lookup table:

var profile = {
	resourceTags: {
		miniExclude: function (filename, mid) {
			return mid in {
				"my/excluded/module": 1,
				"another/excluded/module": 1
			};
		}
	}
};

Check out the dojo.profile.js and dijit.profile.js files for more examples of resourceTags usage.

Layers

There are many situations where it is beneficial to have multiple layer files. The most common cases for this are when you have code with several distinct sections that can be loaded on demand. For example, in a Web mail application that also has a calendaring component, the common code, mail part, and calendar part could all be split into separate layers. This would enable users that only want to use the mail service to avoid loading the calendaring code, whilst also making sure users that use both mail and calendaring don’t download shared code twice. Creating these sorts of layers is extremely simple:

var profile = {
	layers: {
		"app/main": {
			include: [ "app/main" ],
			exclude: [ "app/mail", "app/calendar" ]
		},
		"app/mail": {
			include: [ "app/mail" ],
			exclude: [ "app/main" ]
		},
		"app/calendar": {
			include: [ "app/calendar" ],
			exclude: [ "app/main" ]
		}
	}
};

In the above example, the build system creates three layers: one containing the main application, one containing the mail components, and one containing the calendar components. By excluding the app/mail and app/calendar layers from the app/main layer, those modules (and all their dependencies) are excluded from the main layer. (The same thing occurs in reverse by excluding app/main from the app/mail and app/calendar layers.)

Note that if there are other shared components that are not required by any of the non-excluded dependency chains in app/main, you would need to add them to the list of modules to include in that layer. For example, if both the mail and calendar components used DataGrid, but nothing else in app/main referenced it, it would need to be specified explicitly in the main layer to avoid being compiled in separately to both app/mail and app/calendar:

var profile = {
	layers: {
		"app/main": {
			include: [ "app/main", "dojox/grid/DataGrid" ],
			exclude: [ "app/mail", "app/calendar" ]
		},
		"app/mail": {
			include: [ "app/mail" ],
			exclude: [ "app/main" ]
		},
		"app/calendar": {
			include: [ "app/calendar" ],
			exclude: [ "app/main" ]
		}
	}
};

It’s also possible to create a custom build of dojo.js; this is particularly relevant when using AMD, since by default (for backwards compatibility), the dojo/main module is added automatically by the build system to dojo.js, which wastes space by loading modules that your code does not actually use. In order to create a custom build of dojo.js, you simply define it as a separate layer, setting both customBase and boot to true:

var profile = {
	layers: {
		"dojo/dojo": {
			include: [ "dojo/dojo", "app/main" ],
			customBase: true,
			boot: true
		}
	}
};

The customBase directive prevents dojo/main from being added automatically, and the boot directive ensures that the file includes the necessary AMD loader code. Adding app/main to the include list builds app/main (and its dependencies) directly into dojo.js.

Resource optimization

Dojo 1.7 offers two options for compressing JavaScript using the optimize and layerOptimize properties: shrinksafe, which uses the traditional Dojo code compression utility, and closure, which uses the Google Closure Compiler. Closure Compiler is slower than ShrinkSafe but offers improved compression ratios and performs dead code removal, which is especially important when using staticHasFeatures. Generally speaking, you should use Closure Compiler over ShrinkSafe for your production builds.

The build system can also perform rudimentary compression of CSS files that exist within a package directory by setting cssOptimize to comments. This optimization mode strips comments and newlines, and replaces @import statements with the data from the referenced CSS file in order to reduce the number of HTTP requests.

The final mechanism of interest for code optimization within the Dojo build system is the staticHasFeatures hash map. This map transforms has("foo") calls in the source code to the value provided in the staticHasFeatures map. For example, setting staticHasFeatures: { foo: 0 } would cause all instances of has("foo") to be replaced with 0. In combination with if/else statements and a dead code removing optimizer like Closure Compiler, entire sections of code can be removed from the built version of your application. This is particularly useful for mobile builds, since you can exclude things like IE-specific event handling code. Check out the list of has flags that can be used—and of course, you can always add your own.

Putting it all together

Given a typical application that exists in the app package, a simple build script that builds everything into dojo.js would look like this:

var profile = {
	releaseDir: "/path/to/releaseDir",

	basePath: "..",

	action: "release",

	cssOptimize: "comments",

	mini: true,

	optimize: "closure",

	layerOptimize: "closure",

	stripConsole: "all",

	selectorEngine: "acme",

	layers: {
		"dojo/dojo": {
			include: [ "dojo/dojo", "app/main" ],
			customBase: true,
			boot: true
		}
	},

	resourceTags: {
		amd: function (filename, mid) {
			return /\.js$/.test(filename);
		}
	}
};

This build script performs the following:

  1. Specifies the destination directory for all built files to be /path/to/releaseDir
  2. Specifies the base path of the release as one level above the app package; this example assumes that app is one level below the top-level application directory
  3. Tells the build system to perform a “release”
  4. Strips comments and newlines from CSS files
  5. Marks the build as a “mini” build (so things not necessary for a release—like tests—are not copied)
  6. Minifies JavaScript files using the Closure Compiler
  7. Strips any calls to console functions from the release
  8. Specifies the “acme” CSS selector engine as the default for the application
  9. Builds everything into dojo/dojo
  10. Marks all JavaScript resources in the package as being AMD modules

In addition to this build script, you’ll also need a configuration file that specifies the locations of all the packages used by the application. The format of this package configuration is identical to the format used by the loader, so there are a few different ways to do this. In order of preference:

  1. Use the --require command-line flag to point to a script that contains a require object or a require(config) call
  2. Use the --dojoConfig command-line flag to point to a script that contains a dojoConfig object with package configuration data
  3. Use multiple --package command-line flags to point to each package directory (note that they must all have package.json descriptor files)
  4. Specify the package configurations inside the build profile itself

Running a build

Now that all the various configuration files have been set up, it’s time to actually run a build! Thanks to all of the hard work done preparing for this moment, actually running the build is very simple and looks like this (on Windows, replace ./build.sh with build.bat):

./build.sh --profile /path/to/app/app.profile.js --require /path/to/app/boot.js

This will kick off a build using the build profile we created for the application and the loader configuration. It’s actually possible to specify many different configuration options on the command-line; run ./build.sh --help for a list. It’s also possible to check what the build system thinks it should be using before actually running a build by appending --check-args to the build command. Doing this will output a JSON representation of the processed build configuration.

When running using Closure Compiler, you will see some warnings and errors about dojox code. These can be ignored and will be fixed in a future version of the toolkit.

When running using Node.js 0.6, you will see an error at the end of the build process about process.stdout. This error does not affect the build process and can be ignored.

Conclusion

The build system is critical for deploying Web applications. Even with the asynchronous loading mechanism built into Dojo 1.7, built applications load significantly faster than unbuilt ones. Load time is a key factor in user experience, so don’t release an application without it!

Colophon

Error in the tutorial? Can’t find what you are looking for? Let us know!