Nate Eagle

Front-End Developer

Import a Whole Directory with Sass (Using Grunt)

I’ve been using Compass for almost two years. One of the biggest advantages of using Compass is the ability to separate the many concerns of my CSS into different files. I @import them into the main stylesheet, and they’re all bundled together in a single file when they’re compiled. It’s hard to believe that there was a time I wrote all my CSS in a single file for performance reasons.

Over that time, I’ve gradually refined how I organize my files, especially after reading Jonathan Snook’s SMACSS. One thing I’ve wished I could do, over and over, is have a single import that will pull in a directory full of module files. In other words, I’d like to be able to put @import "modules"; and have it import all of the _somemodule.scss files I have in the modules directory.

However, this is not a feature Sass supports, and I imagine this person is right about why:

This feature will never be part of Sass. One major reason is import order. In CSS, the files imported last can override the styles stated before. If you import a directory, how can you determine import order? There’s no way that doesn’t introduce some new level of complexity. By keeping a list of imports… you’re being explicit with import order. This is essential if you want to be able to confidently override styles that are defined in another file or write mixins in one file and use them in another.

That’s fair enough, but I never want to try to override module styles from other modules. Any dependence on source order in my own CSS would be a flaw, from my perspective; I’d be quite satisfied with an arbitrarily-ordered import of a directory full of module styles.

I could fix this with a shell script, but these days I’m using Grunt JS as my compilation Swiss Army Knife. So I created a simple custom task that will find any _all.scss files and write @import statements to them to import every other unerscore-prefixed file in the same directory. Then I can just add a @import "modules/all"; to my main.scss.

Note: When you prefix files with an underscore, Sass views them as partials, which don’t get compiled to their own css files.

Here’s the custom task, which I saved in a tasks directory off root. (You could also just include the grunt.registerMultiTask without the module.exports in your Gruntfile.js, but why create a mess? Grunt has good organization conventions.)

'use strict';

module.exports = function (grunt) {

    grunt.registerMultiTask(
        'sass-directory-imports',
        'Write SASS @import statements to a single file to include a directory\'s entire contents dynamically.',
        function () {
            var files = this.filesSrc;
            var quiet = this.options().quiet;
            files.forEach(function (filepath) {
                // Create an array that we'll ultimately use to populate our includes file
                var newFileContents = [
                    // Header
                    '// This file imports all other underscore-prefixed .scss files in this directory.',
                    '// It is automatically generated by the grunt compass-directory-includes task.',
                    '// Do not directly modify this file.',
                    ''
                ];
                var directory = filepath.substring(0, filepath.lastIndexOf('/'));

                // Search for underscore prefixed scss files
                // Then remove the file we're writing the imports to from that set
                var filesToInclude = grunt.file.expand([directory + '/_*.scss', '!' + filepath]);

                if (!quiet) {
                    grunt.log.writeln('\n' + filepath.yellow + ':');
                }

                if (!quiet && !filesToInclude.length) {
                    grunt.log.writeln('No files found in ' + directory.cyan + ' to import.');
                }

                filesToInclude.forEach(function (includeFilepath) {

                    // The include file is the filepath minus the directory slash and the
                    // initial underscore
                    var includeFile = includeFilepath.substring(includeFilepath.lastIndexOf('/') + 2);

                    // Remove .scss extension
                    includeFile = includeFile.replace('.scss', '');

                    if (!quiet) {
                        grunt.log.writeln('Importing ' + includeFile.cyan);
                    }

                    newFileContents.push('@import "' + includeFile + '";');
                });

                newFileContents = newFileContents.join('\n');
                grunt.file.write(filepath, newFileContents);
            });
        }
    );
};

Then in my Gruntfile.js I include a block to configure the sass-directory-imports task. I make sure there’s a line loading my project tasks, then I add the sass-directory-imports task to my default. It should be before the compass task, since we want the imports it creates in _all.scss files to be processed by Compass.

// Project configuration.
grunt.initConfig({
    // Other config stuff not relevant to this example
    // ...

    // Custom Task Configuration
    'sass-directory-imports': {
        // This is an arbitrary name for this sub-task
        src: {
            files: {
                // Put an _all.scss file in any directory inside our scss files, and
                // this task will write @import statements for every other _*.scss
                // file in that directory. Then simply @import your _all.scss file to
                // import the contents of the directory.
                src: ['src/scss/**/_all.scss']
            }
        }
    }
});

// Load project tasks
grunt.loadTasks('tasks');

// Default task(s).
grunt.registerTask('default', ['sass-directory-imports', 'compass']);

Simple but satisfying.

Update, April 4: I added some needed improvements to the task, such as better logging (which can be turned off with the quiet option) and a fix for an out-and-out bug that cropped up when multiple files were found by the multi-task.