Skip to content

Instantly share code, notes, and snippets.

@mcmire
Created November 1, 2011 02:46
Show Gist options
  • Save mcmire/1329695 to your computer and use it in GitHub Desktop.
Save mcmire/1329695 to your computer and use it in GitHub Desktop.
Replacing Compass with our own Sass init stuff

"Why replace Compass?" you ask. "Everyone likes Compass. Compass is nice! It provides helpful CSS3 mixins, and it makes life generally easier for the Sass developer."

True, but have you ever looked at the Compass source code? I dare you to figure out how Compass initializes itself, how it parses your configuration file, and how it ties into Rails. You will quickly find that it's an absolute nightmare. There's a Compass::Compiler class which wraps Sass::Plugin::Compiler. Why? I have no clue. There is really no need for Compass, except that it provides an abstraction around Sass for different frameworks. A lot of the stuff in Compass could be in Sass itself!

The other thing that's a little frustrating (and this is more about Sass than Compass) is that paths are too closely tied together. In Sass there are two arrays: the template location array, where you tell Sass where your .sass or .scss files are and where they should end up when they are compiled; and the load path, where you tell Sass where to look when resolving @import statements that refer to partials. Now usually these are the same; if I have Sass files in app/stylesheets, I probably have partials inside of there too.

It gets more complicated when you have more than one directory involved. In our case we have three locations where Sass files could be:

  • app/stylesheets
  • vendor/assets/stylesheets
  • vendor/compass-mixins

However, we want stylesheets in app/stylesheets and vendor/assets/stylesheets to end up in public/stylesheets/compiled, in a certain directory structure such that app/stylesheets/app and vendor/assets/stylesheets/app get merged into public/stylesheets/compiled/app, and so forth. So we have a symlink to guarantee this happens:

  • public/stylesheets/src/app -> app/stylesheets
  • public/stylesheets/src/vendor -> vendor/assets/stylesheets

So when we are compiling Sass files, we want this to happen:

  • public/stylesheets/src/**/*.scss -> public/stylesheets/compiled/**/*.css

Here is how we can achieve this.

  1. Remove Compass from Gemfile, and re-run bundle install
  2. cp -R COMPASS_GEM/frameworks/compass YOUR_RAILS_APP/vendor/compass-mixins
  3. Remove config/compass.rb
  4. Add config/initializers/sass.rb with this content:
module Sass
  module Plugin
    class Compiler
      # Override to NOT put template_location in the load_paths
      def load_paths(opts = options)
        opts[:load_paths] || []
      end
    end
  end
end

module SassCompiler
  class << self
    DEFAULT_OPTIONS = {
      :cache => true,
      :line_comments => true,
      :style => :expanded,
      :css_location => nil,
      :cache_location => Rails.root.join('tmp/sass-cache'),
      :load_paths => [],
      # Set default template location array since
      # Sass::Plugin::Configuration#normalize_template_location! initializes
      # the array with [:template_location, :css_location], but only if it
      # hasn't been set yet
      :template_location => []
    }

    def add_to_template_locations(*template_locations)
      template_locations.each do |pair|
        source_dir, dest_dir = pair.map {|dir| Rails.root.join(dir) }
        # Write to the array directly since
        # Sass::Plugin::Configuration#normalize_template_location! does some
        # weird stuff with the template location array
        _options[:template_location] << [source_dir, dest_dir]
      end
    end

    def add_to_load_path(*dirs)
      _options[:load_paths] |= dirs.map {|dir| Rails.root.join(dir) }
    end

    def updating_plugin_options
      yield
      Sass::Plugin.options.merge!(_options)
    end

    def compile
      Sass::Plugin.force_update_stylesheets
    end

    def clear
      system "rm -rfv #{Sass::Plugin.options[:cache_location]}"
      Sass::Plugin.compiler.send(:css_locations).each do |dir|
        system "rm -rfv #{dir}/*"
      end
    end

    def add_hooks
      Sass::Plugin.on_updating_stylesheet do |sass_file, css_file|
        SassCompiler.on_updating_stylesheet(sass_file, css_file)
      end

      Sass::Plugin.on_compilation_error do |error, sass_file, css_file|
        SassCompiler.on_compilation_error(error, sass_file, css_file)
      end
    end

    def on_updating_stylesheet(sass_file, css_file)
      sass_file = _abbr_path(sass_file)
      css_file = _abbr_path(css_file)
      logger.debug "Sass compiled #{sass_file} to #{css_file}"
    end

    def on_compilation_error(error, sass_file, css_file)
      sass_file = _abbr_path(sass_file)
      css_file = _abbr_path(css_file)
      logger.debug "Sass ran into a #{error.class} error while compiling #{sass_file} to #{css_file}: #{error.message}"
    end

    def _options
      @_options ||= DEFAULT_OPTIONS.dup
    end

    def _abbr_path(path)
      path.sub(Rails.root.to_s + "/", "")
    end
  end
end

SassCompiler.updating_plugin_options do
  SassCompiler.add_to_template_locations(['public/stylesheets/src', 'public/stylesheets/compiled'])
  SassCompiler.add_to_load_path('app/stylesheets', 'vendor/assets/stylesheets', 'vendor/compass-mixins')
end
SassCompiler.add_hooks
  1. Add lib/tasks/sass.rake with this content:
namespace :sass do
  task :compile => [:clear, :environment] do
    SassCompiler.compile
  end

  task :clear => :environment do
    SassCompiler.clear
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment