Injecting variables during SASS compilation with Node

17,910

Solution 1

I found myself in a very similar situation. We had a lot of existing SASS that now needed to accept dynamic values/variables to be used throughout (as variables). I originally went down the route of writing temporary directories/files and essentially creating a "proxy entry point" which would create a proxy_entry.scss and variables.scss and bootstrap the actual entry.scss with intended SASS variables declared. This worked fine and achieved the desired results, but it felt a bit overcomplicated...

It turns out there is a much simpler solution available thanks to node-sass's options.data option. This accepts a "SASS string to be evaluated".

Type: String Default: null Special: file or data must be specified

A string to pass to libsass to render. It is recommended that you use includePaths in conjunction with this so that libsass can find files when using the @import directive.

This completely eliminated the need for writing/managing all of the temporary directories and files.

Visual TL;DR

Dynamic Variables in SASS with node-sass

The solution boils down to something like this

1.) Define sassOptions as usual

var sassOptionsDefaults = {
  includePaths: [
    'some/include/path'
  ],
  outputStyle:  'compressed'
};

2.) Write the "dynamic SASS string" for options.data

var dataString =
  sassGenerator.sassVariables(variables) +
  sassGenerator.sassImport(scssEntry);
var sassOptions = _.assign({}, sassOptionsDefaults, {
  data: dataString
});

3.) Evaluate the SASS as usual

var sass = require('node-sass');
sass.render(sassOptions, function (err, result) {
  return (err)
    ? handleError(err);
    : handleSuccess(result.css.toString());
});

Note: this is assuming your entry.scss imports some variables.scss that defines variables as "defaults".

// variables.scss
$someColor: blue !default;
$someFontSize: 13px !default;

// entry.scss
@import 'variables';
.some-selector { 
  color: $someColor;
  font-size: $someFontSize;
}

Piecing it all together as an example

var sass = require('node-sass');

// 1.) Define sassOptions as usual
var sassOptionsDefaults = {
  includePaths: [
    'some/include/path'
  ],
  outputStyle:  'compressed'
};

function dynamicSass(scssEntry, variables, handleSuccess, handleError) {
  // 2.) Dynamically create "SASS variable declarations"
  // then import the "actual entry.scss file".
  // dataString is just "SASS" to be evaluated before
  // the actual entry.scss is imported.
  var dataString =
    sassGenerator.sassVariables(variables) +
    sassGenerator.sassImport(scssEntry);
  var sassOptions = _.assign({}, sassOptionsDefaults, {
    data: dataString
  });

  // 3.) render sass as usual
  sass.render(sassOptions, function (err, result) {
    return (err)
      ? handleError(err);
      : handleSuccess(result.css.toString());
  });
}

// Example usage.
dynamicSass('some/path/entry.scss', {
  'someColor': 'red',
  'someFontSize': '18px'
}, someSuccessFn, someErrorFn);

Where the "sassGenerator" functions could looks something like

function sassVariable(name, value) {
  return "$" + name + ": " + value + ";";
}

function sassVariables(variablesObj) {
  return Object.keys(variablesObj).map(function (name) {
    return sassVariable(name, variablesObj[name]);
  }).join('\n')
}

function sassImport(path) {
  return "@import '" + path + "';";
}

This enables you to write your SASS just as you did before, using SASS variables anywhere that they are needed. It also doesn't tie you down to any "special dynamic sass implementation" (i.e. this avoids using "underscore/lodash templating throughout your .scss files). It also means you can take advantage of IDE features, linting, etc... just the same since you are now just back to writing regular SASS.

Additionally, it translates nicely to non-node/http/compile-on-the-fly usages such as pre-compiling multiple variations of entry.scss given multiple value sets via Gulp, etc...

I hope this helps you @ChrisWright (and others) out! I know I struggled finding information on the subject and I imagine this is a fairly common use-case (wanting to pass dynamic values into SASS from a Database, config, HTTP parameters, etc...).

Solution 2

I was able to solve this after I wrapped my head around node-sass' importer() method. My solution involved underscore templates and manually reading files as they come in. It's not the most elegant or efficient solution, but it is only run once per generated page. After that, files are minified and cached for future requests.

// Other none-sass render parameters omitted for brevity
importer: function (url, prev, done) {

    // Manually read each partial file as it's encountered
    fs.readFile(url, function (err, result) {
        if (err) {

            // If there is an error reading the file, call done() with
            // no content to avoid a crash
            return done({
                contents: ''
            });
        }

        // Create an underscore template out of the partial
        var partial = _.template(result.toString());

        // Compile template and return its contents to node-sass for
        // compilation with the rest of the SCSS partials
        done({
            contents: partial({ data: YOUR_DATA_OBJECT })
        });
    });
}

Using this solution, we are able to reference normal underscore variable syntax inside our SCSS partials. As an example:

body {
    color: <%= data.colour %>;
}
Share:
17,910

Related videos on Youtube

Chris Wright
Author by

Chris Wright

Updated on June 05, 2022

Comments

  • Chris Wright
    Chris Wright almost 2 years

    In an application I'm working, I have to dynamically compile SASS before rendering on the client (caching system is coming, don't worry). Currently I'm using node-sass and everything is working great.

    Here is what I'm working on so far. Other project-specific code has been removed for brevity:

    var sass            = require('node-sass'),
        autoprefixer    = require('autoprefixer-core'),
        vars            = require('postcss-simple-vars'),
        postcss         = require('postcss'),
    
    function compileCSS() {
        var result = sass.renderSync({
                file: 'path/to/style.scss'
            });
    
        return postcss([autoprefixer]).process(result.css.toString()).css;
    }
    

    The wrinkle is that now I need to pass in dynamic data from Node and have that compile like a normal SASS variable. Initially I tried using PostCSS, because I noticed that variable injection was something it could do. Unfortunately, that didn't work. PostCSS kicks in after the compilation phase, which fails miserably by this point.

    Next, I tried to use underscore templates to try and overwrite using node-sass' importer():

    var result = sass.renderSync({
            file: 'path/to/style.scss',
            importer: function(url, prev, done) {
                var content = fs.readFileSync(partial_path),
                    partial = _.template(content.toString());
    
                return {
                    contents: partial({ test: 'test' })
                };
            }
        });
    

    Which resulted in the following error:

    Error: error reading values after :
    

    Obviously SASS didn't like underscore's variable syntax..


    TL;DR

    How can I pass dynamic variables to SASS from within my Node application?


    Additional Information

    1. My team and I are not completely adverse to switching to something like Stylus; however, we have made significant progress so far and it would be a pain.
    • cimmanon
      cimmanon almost 9 years
      Wouldn't it just be easiest to have your application write out a file that Sass can import? Why do you need to inject it dynamically?
    • Chris Wright
      Chris Wright almost 9 years
      Are you suggesting a kind of temporary "var" file?
    • cimmanon
      cimmanon almost 9 years
      That would be the simplest way to do it, yes.
    • Erik Aybar
      Erik Aybar almost 9 years
      "Are you suggesting a kind of temporary "var" file?" @ChrisWright This is exactly the scenario and approach I am in and in the midst of trying to solve. I'm currently looking at writing 2 files on the fly. Example: ['variables.scss', 'import.scss'] where import.scss imports the variables and then the rest of the SCSS as usual. I'm just starting this project now. I would love to hear if you've had any luck!
    • Erik Aybar
      Erik Aybar almost 9 years
      I did get a fully working solution for this up and going today. It does involve temp files/directories. I'll see about cleaning it up and posting as an answer sometime early next week @ChrisWright Our "variables" come from HTTP request params, but the solution would work for database values just the same.
    • Chris Wright
      Chris Wright almost 9 years
      @ErikTheDeveloper I've posted my solution, but I'd love to see what you came up with.
  • ObjectiveTruth
    ObjectiveTruth over 8 years
    Great answer, @ErikTheDeveloper, I'm surprised no up votes
  • Chris Wright
    Chris Wright over 8 years
    I agree this solution looks pretty good. Unfortunately I haven't had time during our sprints to test it out. When I do, I will mark this as the official answer once I know it works.
  • Syed
    Syed over 8 years
    ErikTheDeveloper is it possible, you could post a grunt version of your code? and also in the form of working code in gist?
  • Eliran Malka
    Eliran Malka over 7 years
    nice technique, but it's worth noting that it adds another level of complexity; i have to change my current entrypoint stylesheet and any reference to it. also, if my app has several modules (and several entrypoints) i have to repeat the process for every entrypoint file.. this post addresses the same issue with a more simple approach (that suited me better).
  • wayofthefuture
    wayofthefuture about 7 years
    This is a really intuitive solution. Does anyone know how we could then implement this into webpack?
  • alpham8
    alpham8 almost 7 years
    do you have an idea how do I use this in grunt-sass? github.com/sindresorhus/grunt-sass
  • montrealist
    montrealist almost 7 years
    FYI this doesn't work if using gulp-sass: github.com/dlmanning/gulp-sass/issues/302
  • 3stacks
    3stacks over 6 years
    I know this is old, but this is my favourite answer on SO. kudos
  • avcajaraville
    avcajaraville over 6 years
    Wow, this was a really amazing answer !! Thanks so much !!
  • Chris
    Chris over 5 years
    This was a game changer for me. Thank you for this.
  • Julien Ducro
    Julien Ducro over 4 years
    It looks like grooveui.com is not around anymore
  • Dragod83
    Dragod83 about 3 years
    how do i call the sassGenerator function? ReferenceError: sassGenerator is not defined