Bower support in Metalsmith

In my previous post, I expressed my love for Metalsmith, but I also mentioned that I had to add some plugins myself to make it do exactly what I wanted it to do.

One of the things I wanted to have is support for Bower. Since I started using Brunch, I also started using Bower, a lot. However, Brunch doesn't always add the files I want to include, or it includes too much. There were times where I wished I just had a little bit more control over what got included.

With Metalsmith, you have all the control you want. To be fair, it doesn't give you anything to support Bower, so you actually need to set it all up yourself. But doing that, actually turns out to be pretty easy. Note that I assume you will configure Metalsmith programmatically. If you're note, you're out of luck – for now.

(In case you're not familiar with Bower or Metalsmith: Bower is a tool managing the JavaScript, CSS and other libraries you want to include client side. Metalsmith is minimal static web site generator.)

How to

Step 1: Install bower-files

npm install -S bower-files

Step 2: Require bower-files

lib = require('bower-files')()

If you didn't have it yet, also make sure you have the readFileSync and basename functions available, since we'll be using that as well:

{ readFileSync } = require 'fs'
{ basename } = require 'path'

Step 3: Add the plugin

This might not be the usual way you add plugins. In this case, we're going to define a plugin inside the file you're using to configure Metalsmith.

bower = (files, metalsmith, done) ->
  include = (root, included) ->
    for file in included
      contents = readFileSync(file)
      files["#{root}/#{basename(file)}"] =
        contents: contents
  include('css', lib.self().ext('css').files)
  include('js', lib.self().ext('js').files)
  include('fonts', lib.self().ext(['eot','otf','ttf','woff']).files)
  done()

Let's take this code apart, to explain what it's doing. First of all, a plugin in Metalsmith is just a function with a particular signature. The files parameter gives you all the files is gathered so for, the metalsmith parameter allows you to access Metalsmith's API, and the done parameter is the callback function you need to call when your plugin is done.

bower = (files, metalsmith, done) ->

The next few lines define a function that accept two parameters. The first parameter defines the root of the folder where we want to move the files. The other parameter is a list of files to be moved to that location. Bear in mind that the first parameter points is just a String, pointing at a folder relative to the root of the virtual file system managed by Metalsmith. The second argument, the lists of files to include is an array of absolute file names.

  include = (root, included) ->
    for file in included
      contents = readFileSync(file)
      files["#{root}/#{basename(file)}"] =
        contents: contents

Inside the body of the include function, every file in the list of included files first gets its contents read into a temporary variable. Once that's done, a new file is getting added to Metalsmith's collection of files, taking the basename of the original file and moving it into the type specific root for this file.

The last bit of code is the piece that actually calls the include function for all the type of files I want to get included in the files managed by Metalsmith:

  include('css', lib.self().ext('css').files)
  include('js', lib.self().ext('js').files)
  include('fonts', lib.self().ext(['eot','otf','ttf','woff']).files)
  done()

As a result, all of the font files will be in the /fonts directory of the web site produced by Metalsmith.

Step 4: Using the plugin

If you would have completed the previous steps, you would only have the plugin defined. You wouldn't actually be using it. So let's make sure it's also getting used:

metalsmith(__dirname)
  .use(bower)
  .destination('./build')
  .build (err, files) ->
  	if err
  	  console.error err

Conclusion

I have to admit: I was surprised to find out that it only took a few lines of code to get me exactly what I wanted. With Brunch, I still had to jump through a few hoops to make things work ok-ish. But with Metalsmith's simple API, I got it to work exactly how I wanted it.

Now, the plugin I created is not really a standalone plugin that you can npm install, as you normally would. However, getting it to that state really isn't a lot of work, and something I might start doing one of these days, if I have the time.