Most programming systems offer a straightforward way for the code in one file to depend on the code defined in another. Examples include Python’s import statement, Ruby’s require method, or the #include directive in C and C++.
The beauty of this feature is the way it supports modularity. Related code statements – ones describing a bee, for example – can be organized into a discrete file named bee and kept separate from unrelated statements, say those representing a flower. The syntax of the language then allows the programmer to write a statement in the source code of bee that ensures the code placed into the flower file will be available for the other statements in bee to use.
A cascade of benefits follow from such a feature. A separate file named hummingbird may now reuse the code in flower without including any code from bee. Better yet, the flower code may depend another file named water without the code in bee or hummingbird even knowing about it – a pattern known as information hiding. If flower later decides it also needs to depend on a file called sunlight, no adjustments need to be made to the code in bee or hummingbird since flower’s dependencies are hidden from them.
Well-designed systems go a step further, abstracting module identifiers from the path names to specific source code files. It becomes the job of linker or interpreter to figure out which source file provides the code for the flower module. Usually, such systems scan through a prearranged list of directories to locate the appropriate file. In this way, module files can be reorganized into different directories without needing to change any source code.
When it comes to specifying dependencies between HTML, CSS, and javascript files, well, what can I say? CSS offers the @import statement, but it isn’t abstracted from the pathname of external assets, and many have criticized its poor performance. Javascript provides no dependency syntax whatsoever, at least in today’s web browsers.
It generally falls to the HTML to link all the requisite assets together. This is rather like asking the bee to know the location of the flower code, as well as all the flower’s dependencies, as well as the location of those dependencies, and so on. Information hiding falls by the wayside.
Instead of keeping dependency information in the files that know it best – the CSS and javascript source files – the entire dependency tree has to be promoted into the HTML. If I add code to a javascript file a.js that requires a new javascript file b.js to be loaded first, I now have to locate every HTML file that links to a.js and prepend it with a link to b.js.
There has to be a better way.
So I created one.
I aspired to a few, simple design principles. All source code had to remain valid: HTML, CSS, and javascript source code had to stay that way, suitable to be run in a browser or given to other people as-is. Buying into my system had to be completely optional, and it needed to be compatible with other frameworks, YUI in particular. Finally, any dependency information needed to be documentable: I wanted human beings or documentation programs to be able to look at my code and get a clear picture of what other assets were needed to make everything work.
To pull this off, I developed a simple dependency syntax to be inserted into CSS or javascript comments. The form looks like this:
/* :depends module1, module2, … */
Sticking to this modest convention has made a tremendous difference in the ease with which I create professional web pages today. My HTML source typically contains just one <style> and <script> tag, each with a dependency comment referencing just one external module:
<!DOCTYPE html>
<html lang=en>
<head>
<title>example</title>
<style>/*:depends example*/</style>
</head>
<body>
<h1>example</h1>
<script>/*:depends example*/</script>
</body>
</html>
Obviously, code like this makes it a breeze to review the quality of my markup – independent of CSS presentation or javascript behaviour. I just load the HTML file into my web browser as-is.
More importantly, this approach restores the appropriate level of trust to each source file to know its own dependencies. The above code depends on two files, example.css and example.js. Near the top of my example.js file, I might add a comment like the following were it to expect code in the YUI dom.js or event.js files to be loaded first:
/* :depends dom, event */
I’ve downloaded yahoo.js, dom.js, and event.js from the YUI project into my ~/lib/js directory. Since dom.js and event.js depend on yahoo.js, I’ve inserted this comment at the top of each:
/* :depends yahoo */
To stitch it all together, I’ve written a Python module called htmldeps.py, which I run from directly from the command line or import into a Python build script. I make sure it knows to look in ~/lib/js for the YUI files, then I pass it the HTML and specify an output directory, into which htmldeps writes the following along with all CSS and javascript files referenced:
<!DOCTYPE html>
<html lang=en>
<head>
<title>example</title>
<link rel="stylesheet" href="example.css">
</head>
<body>
<h1>example</h1>
<script src="yahoo.js"></script>
<script src="dom.js"></script>
<script src="event.js"></script>
<script src="example.js"></script>
</body>
</html>
For high-performance, htmldeps may be run with an option that flattens each chain of links into one link. The link references a file (named the same as the last file in the chain) containing the concatenation of all previously linked code:
<!DOCTYPE html>
<html lang=en>
<head>
<title>example</title>
<link rel="stylesheet" href="example.css">
</head>
<body>
<h1>example</h1>
<script src="example.js"></script>
</body>
</html>
Here, example.js would contain the concatenation of yahoo.js, dom.js, event.js, and example.js.
It’s worth noting that I use this approach only for assembling the minimum, static dependencies needed to get a page up and running. Additional code may be loaded on demand via XHR.
Any build system, in any language, could be extended to hook into the dependency comments I describe here. If you’d like to check out or use the home baked code I use to pull it all together, I’ve registered htmldeps.py with the Python package index under an open source license for anyone to use. Enjoy!