Advanced sass handling with vueify

Introduction

Vueify enables using .vue files with browserify. Inside them, one can use sass/scss to write the style definitions.

This works pretty well but I recently recognized a drawback. Since all .vue files get compiled separately, some sass features like combining extended selectors get lost. This can lead to increased css file sizes because extended classes and placholders will be duplicated all over the file.

Example

Let's imagine we have a _placeholders.scss which defines reusable placeholders:

/* _placeholders.scss */

%boldText {
    font-weight: bold;
}

Now we also have two vue components which use that placeholder:

<!-- firstComponent.vue -->
...
<style lang="scss">
@import 'placeholders';

.text {
    @extend %boldText;
}
</style>
...

<!-- secondComponent.vue -->
...
<style lang="scss">
@import 'placeholders';

.some-other-text {
    @extend %boldText;
}
</style>
...

Normally, sass would create the following css:

.text, .some-other-text {
    font-weight: bold;
}

Due to the fact that every file gets compiled separately, this actually doens't happen. The resulting css instead is

.text {
    font-weight: bold;
}

.some-other-text {
    font-weight: bold;
}

The solution

One way to get around this is to let vueify create a combined sass file with the styles of all the separate components. This file can then be compiled via node-sass.

Create the sass file

First we need to tell vuefiy, that it should create a separate file for the styles. This can be done with the extract-css plugin. Since vueify 9, it's shipped with the vueify npm package. Older versions require the plugin to be installed separately. Further information can be found here.

To use this plugin, just pass it to browserify:

browserify -p [ vueify/plugins/extract-css -o dist/app.css ] ...

By default the plugin compiles the style definitions directly to css. Fortunately we can prevent vueify from that by defining a custom compiler.

To do that, one just has to create a file called vue.config.js in the project root. Vueify automatically uses this file for it's transformations.

Inside that file we define our custom compiler for the language we want to compile. In my case it's scss. This compiler does nothing more than just pass the styles back to vuefiy as they are. This leads to a combined scss file containing all the component styles.

/* vue.config.js */
let config = {};

if (process.env.NODE_ENV === 'production') {
    config = {
        customCompilers: {
            scss: function (content, cb, compiler, filePath) {
                cb(null, content);
            }
        }
    }
}

module.exports = config;

As you can see, I only do that, when the NODE_ENV is set to production. This is because I only want that file be created for the production build, since in development mode I use hot module replacement which only works with the embedded styles.

When you now run browserify you get a bundled sass file with all the component styles. You probably want to change the file extension in the browserify parameters.

browserify -p [ vueify/plugins/extract-css -o dist/app.scss ] ...

Compile the sass file

Now that we have our bundled sass file, we can compile it just like any other sass file. Since in my case the file is in another directory than the original code, I have to set the include path to ensure sass can find my imported files.

node-sass --output-style compressed --include-path src dist/app.scss > dist/app.css

When you compile that file you might notice that there will be duplicate css definitions for the used placeholders. This is a problem of sass. When you define the same placeholder multiple times, sass will create another css definition for every instance of the placeholder. Since we have now multiple import statements in the target sass file, we also have multiple placeholder definitions for the same placeholders.

We will solve that problem in the next section.

Deduplicate sass imports

To ignore the duplicate import statements in the bundled sass file we use a handy option, which can be passed to node-sass: the importer.

With this, one can tell node-sass how to handle imports. Actually one would never touch that but in this case we can use this to tell sass to ignore the duplicates.

I came up with this importer function:

/**
* importer.js
* 
* An importer function which can be used with node-sass.
* It ensures that multiple import statements in sass/scss files get only imported once.
*/

'use strict';

var readFile = require('fs').readFile;
var basename = require('path').basename;

module.exports = (function () {
    // storage of already imported files
    var $imports = [];

    // If file was already imported, return an empty string.
    // Otherwise return the filename to node-sass to let it be imported.
    return function (url) {
        // use only the filename because relative paths are hard to handle
        // since we don't know, where the original file was
        var filename = basename(url);

        if ($imports.indexOf(filename) < 0) {
            $imports.push(filename);

            return {
                file: url
            };
        }

        return {
            contents: ''
        };
    };
}());

In the function, I store all the urls of the found imports into an array. When an url is used the first time it gets loaded and passed back to sass. When an import is loaded a second time an empty string gets returned.

To use that importer function just save it somewhere and pass it to node-sass:

node-sass --output-style compressed --include-path src --importer importer.js dist/app.scss > dist/app.css

That's it. Now the duplicate entries are gone. I also created an npm module for that which can be used right away.

Happy coding!