Detecting LESS Changes with Middleman

The combination of Middleman, LESS, and Bootstrap is perfect for web application prototyping. Kedron Rhodes wrote about using Middleman and Bootstrap for rapid prototyping last year, and recently Dustin Tinney wrote about how to use LESS mixins to reduce some of Bootstrap’s clutter.

Configuring Middleman to use LESS is simple. Just add gem 'less' and gem 'therubyracer' to your Gemfile, and you can start requiring .less files in the asset pipeline (see Asset Pipeline in the Middleman docs).

The Problem

Everything seems to work great until you start using LESS‘s import capability to import other .less files. If you make a change to a file that has been imported into another LESS stylesheet, Middleman does not detect the change. This means you can change the styles all day long but you won’t see your changes when you refresh the browser.


issue was opened on GitHub earlier this year about the problem but has since been closed without being fixed. I would like to eventually dig into it and try to fix the problem, but until I get a chance to do that, I came up with a workaround that lets me move forward right now.

The Solution

1. Require a Single LESS File

In order to take advantage of the full power of LESS, it makes sense to have a single LESS file for your application that uses “@import” statements to pull in all of your other LESS files. The CSS generated from that one LESS file can then be tacked on to any other CSS you want to include by requiring it in the asset pipeline. For example, here is an all.css that includes some jquery-ui styles and the contents of an application.css.less file:

/*
 *= require jquery-ui/jquery-ui
 *= require application
 */

And here is an example of an application.css.less file:

@import 'lib/bootstrap-3.0.0/bootstrap.less';

@import 'my-variables';
@import 'layout';
@import 'pages/home';

Now keep in mind with this setup that, if a change is made to layout.less, it will not be detected by Middleman. Only changes to a LESS file that are required directly through Sprockets get picked up.

2. Add a Little Misdirection

The key to this workaround is to “trick” Middleman into detecting changes by modifying the application.css.less file every time one of the other LESS files is updated. Instead of importing the other files directly in application.css.less, let’s add another file called site.less that will have all of the import statements.

So now we have an application.css.less file:

@import 'site'

// 1

And site.less:

@import 'lib/bootstrap-3.0.0/bootstrap.less';

@import 'my-variables';
@import 'layout';
@import 'pages/home';

Notice the commented out number at the bottom of the application.css.less file? The purpose of that is to give us something that can be updated each time we want Middleman to recompile the LESS files. All you have to do is increment the number each time you make a change to a LESS file.

3. Automate It

Obviously having to manually update some file each time you make a change is not going to be a pleasant development cycle. This is where Guard comes in. The following Guardfile will configure it to watch for changes to any .less stylesheets and automatically modify the application.css.less file, causing Middleman to recompile the stylesheets on the next page load.

require 'guard/guard'

module ::Guard
  class LessWatcher < ::Guard::Guard

    def start
      run_on_changes([])
    end

    def reload
      start
    end

    def run_on_changes(paths)
      dest_path = "source/stylesheets/application.css.less"
      return if paths == [dest_path]

      next_number = if File.exists?(dest_path)
                     contents = File.read(dest_path)
                     if contents =~ %r{// (\d+)}
                       $1.to_i + 1
                     end
                   end

      next_number = next_number.to_i

      template = File.read("source/stylesheets/application-template.css.less")

      File.open(dest_path, 'w') do |file|
        file.write(template.gsub(%r{// (\d+)}) { "// #{next_number}"})
      end
    end
  end
end

guard :less_watcher do
  watch(%r{^source/stylesheets/(.+)\.less$})
end

And here is the template file that is used by the Guardfile (source/stylesheets/application-template.css.less):

// GENERATED FILE - DO NOT MODIFY
@import 'site';

// 0

4. Combine Processes with Foreman

At this point, if both Middleman and Guard are running, we can make a change to a .less file, refresh the browser, and the updated styles will be used. Having to deal with two separate processes is a pain, but with Foreman this is no problem. Here’s a Procfile that will fire up Middleman and Guard when run:

guard: bundle exec guard start -i
middleman: bundle exec middleman server

The following command will run Foreman (assuming it is in your Gemfile):

bundle exec foreman start

5. Ignore the Generated File

You will want to add the generated source/stylesheets/application.css.less file to your .gitignore (or whatever the equivalent is for your source control system). The Guardfile will ensure that the generated file exists whenever it starts, so there’s no need for the file to be checked in. And ignoring the generated file will prevent endless conflicts when multiple developers are working in the codebase at the same time.

6. Building the Static Site

Middleman treats its source files differently when it builds the static site, as opposed to running a local development server. In order to keep it from trying to compile all of the LESS files it comes across in the source/stylesheets directory, you have to configure it to ignore them. You can do this by adding an ignore line in config.rb:

configure :build do
  ignore "*.less"
end

The last thing you should consider doing is adding a Rake task to make sure the application.css.less file exists before building the static site. In normal development, the file would be there as soon as you start Guard, but if you wanted to have a Continuous Integration server build the site automatically each time a change is committed, then Guard would not be involved.

task :copy_template do
  sh "cp source/stylesheets/application-template.css.less source/stylesheets/application.css.less", verbose: false
end

desc "Build/compile the site"
task :build > :copy_template do
  sh "bundle exec middleman build --clean"
end

Put It All Together

I’ve created a skeleton Middleman project (including Bootstrap 3) that shows how all of these pieces fit together. Check it out if you want an example of how Middleman, Guard, and Foreman can work together to make for a hassle-free prototyping environment with LESS.

Conversation
  • Casey says:

    I don’t suppose you ever managed to get a proper fix for this issue in Middleman?

  • Patrick Bacon Patrick Bacon says:

    Casey,

    No, I never did. This workflow worked well enough that I was never forced to get to the bottom of the real issue.

  • This worked well for me, however a slight modification modification to the Guardfile was required to use the newest version.
    Update:
    class LessWatcher < ::Guard::Guard
    to
    class LessWatcher < ::Guard::Plugin

  • Comments are closed.