richard.antecki.id.au
May 20th 2013

Better stylesheet includes in Docpad


I, as with most people I imagine, like breaking up my Stylus files into multiple files and using @import to include them into the main screen.css.styl or whatever. The included files themselves are only relevant during the asset generation process and should not be written to the output directory, but Docpad makes it surprisingly hard for you to do this, insisting on writing redundant files to the generated site /out directory.

A lesser pedantic person could probably easily ignore this, as they're not really doing any harm. But this particular issue has been gnawing at me for a while so I've experimented with a variety of methods over the last couple of months to try and get around this.

Option #1: ignoreCustomPatterns

The first possibility that jumped out at me was using the ignoreCustomPatterns configuration item to force Docpad to ignore these files. I renamed all the included .css.styl files to .inc.styl and changed my config to the following:

    ignoreCustomPatterns: /(~$)|(.ignore$)|(.inc.styl$)/

This method effectively stops Docpad from acknowledging that the .inc.styl files are documents and they don't get processed or written to the /out directory on generation. Great! The only problem however is that Docpad now refuses to watch them for file changes, which can get a tad annoying during development as you now have to restart Docpad whenever you change one of them to force it to update.

It was back to the drawing board.

Just as a side note: the (.ignore$) pattern is a handy method I use to temporarily disable a document from being included in the generated output (for drafts etc...) by appending an additional .ignore to the extension. I find this easier than playing around with enabled=False or active=true metadata or similar.

Option #2: Post-generation cleanup

The next option that occured to me was to just run a cleanup process after generation that deleted all the resultant .inc files from the /out directory.

This option certainly works and I use a similar method via Grunt's clean plugin to remove .js files after they've been concatenated and uglified, but it just didn't seem right for the Stylus includes. The .js files after all are perfectly valid files to be in the /out directory and the website can use them directly when run in a development environment. But the .inc.styl files were something I felt should never be there. It feels like a bandaid to me.

I just didn't like it.

Option #3: Via a partials-like plugin

Docpad's Partials plugin was made to solve a very similar problem, but for html includes. It's strategy is to use a completely separate /partials directory which lives inside the top-level docpad directory, outside both /files and /documents. So the obvious question to ask is: could we use a similar mechanism for stylesheet include files?

The Partials plugin works via the @partial function which you call via your templates, which knows about the /partials directory and how to get files from it. In order to get files from a separate directory via a Stylus @import we'd have to either do something like this:

@import '../../stylus-includes/stuff.inc.styl'

Ugh. Hard-coded relative paths are a recipe for maintainence nightmares. To make it a bit more palatable we could tack on a .eco to the file extension and do this:

@import '<%= @stylus-include("stuff.inc.styl") %>'

Where @stylus-include is just a template helper that returns the relative path to the given include file as prescribed above.

It's definitely cleaner, but now our main stylesheet file is called screen.css.styl.eco. I appreciate Docpad's asset pipeline as much as the next person, but this is starting to get a bit ridiculous for such a small benefit, no?

Another thing I don't like about this is that now the include files are physically separate from the main stylesheet file, which is confusing and inelegant.

Finally, like the Partials plugin, we'd also needs to replicate a whole bunch of Docpad plumbing in order to get our include files included in Docpad's file watching system. It just looked like a lot of messing around.

There had to be a better way.

The winner: Via a renderer plugin

A more recent thought I had was to write a renderer plugin for the .inc.styl extension, which would somehow skip the rendering process altogether. This could be expanded to a generic .inc.* renderer that could be used for any stylesheet pre-processor language or other similar situations. It sounded perfect.

The first thing I had to work out was how to stop Docpad from writing a file, so I started toying around with ideas. I thought that maybe returning null content from the plugin would trick Docpad into skipping the file. Nope, didn't work. Returning the empty string resulted in an empty file being written too.

Then I discovered that setting write = false in the document attributes did exactly what I wanted. So I hacked together a quick plugin to do this:

module.exports = (BasePlugin) ->
    class IgnoreIncludes extends BasePlugin
        name: 'ignore-includes'

        config:
            ignoredExtensions: ['inc']

        render: (opts, next) ->
            {inExtension,outExtension} = opts
            ignoredExtensions = @config.ignoredExtensions

            if ignoredExtensions? and outExtension in ignoredExtensions
                opts.file.attributes.write = false
                # avoid 'didnt do anything' warning
                opts.content = ""

            next()

Short and sweet. Note that I had to change the document content too or else Docpad complains with a "didn't do anything" warning. So I just set it to an empty string.

I also made it a little bit configurable by allowing you to configure the output extensions to ignore.

Finally, a nice clean /out directory!

You can grab this plugin on github here or via npm:

npm install docpad-plugin-ignoreincludes