Advertisement

When and How to Support Multiple Versions of Sass

by

The other day, I was reviewing Jeet grid system's Sass code, just for the sake of it. After some comments on the GitHub repository, I understood Jeet's maintainers were not ready to move to Sass 3.3 yet. In actual fact, it's more accurate to say Jeet users aren't ready to move to Sass 3.3, according to the number of issues opened when Jeet started using Sass 3.3 features. 

Anyway, the point is Jeet can't get all the cool and shiny stuff from Sass 3.3. Or can it?

*-exists Functions

If you are aware of what version 3.3 brought to Sass, you might know that a couple of helper functions have been added to the core, aimed at helping framework developers support multiple versions of Sass at the same time:

  • global-variable-exists($name): checks whether a variable exists in global scope
  • variable-exists($name): checks whether a variable exists in current scope
  • function-exists($name): checks whether a function exists in global scope
  • mixin-exists($name): checks whether a mixin exists in global scope

There is also a feature-exists($name) function, but I'm really not sure what it does since the docs are quite evasive about it. I even took a glance at the function's code, but it doesn't do more thanbool(Sass.has_feature?(feature.value)), which doesn't help much.

Anyway, we have a couple of functions able to check whether a function, a mixin or a variable exists, and that's pretty nice. Time to move on.

Detecting Sass Version

Okay, new functions, pretty cool. But what happens when we use one of those functions in a Sass 3.2.x environment? Let's find out with a little example.

// Defining a variable
$my-awesome-variable: 42;

// Somewhere else in the code
$does-my-awesome-variable-exist: variable-exists('my-awesome-variable');
// Sass 3.3 -> `true`
// Sass 3.2 -> `variable-exists('my-awesome-variable')`

As you can see from the results, Sass 3.2 doesn't crash or throw any error. It parses variable-exists('my-awesome-variable') as a string, so basically "variable-exists('my-awesome-variable')". To check whether we're dealing with a boolean value or a string, we can write a very simple test:

$return-type: type-of($does-my-awesome-variable-exist);
// Sass 3.3 -> `bool`
// Sass 3.2 -> `string`

We are now able to detect the Sass version from within the code. How awesome is that? Actually, we don't exactly detect the Sass version; rather we find a way to define if we're running Sass 3.2 or Sass 3.3, but that's all we need in this case.

Progressive Enhancement

Let's look at bringing progressive enhancement to Sass functions. For instance, we could use native tools if they are available (Sass 3.3), or fall back to custom ones if they are not (Sass 3.2). That's what I suggested to Jeet regarding the replace-nth() function, which is used to replace a value at a specific index.

Here is how we could do it:

@function replace-nth($list, $index, $value) {
  // If `set-nth` does exist (Sass 3.3)
  @if function-exists('set-nth') == true {
    @return set-nth($list, $index, $value);
  }

  // Else it's Sass 3.2
  $result: ();
  $index: if($index < 0, length($list) + $index + 1, $index);  

  @for $i from 1 through length($list) {
    $result: append($result, if($i == $index, $value, nth($list, $i)));
  }

  @return $result; 
}

And then I suppose you're like... what's the point of doing this if we can make it work for Sass 3.2 anyway? Fair question. I'd say performance. In our case, set-nth is a native function from Sass 3.3, which means it works in Ruby, which means it's way faster than a custom Sass function. Basically, manipulations are done on the Ruby side instead of the Sass compiler's.

Another example (still from Jeet) would be a reverse function, reversing a list of values. When I first released SassyLists, there was no Sass 3.3 so reversing a list would mean creating a new list, looping backwards over the initial list, appending values to the new one. It did the job well. However, now that we have access to the set-nth function from Sass 3.3, there is a much better way of reversing a list: swapping indexes.

To compare performance between both implementations, I tried reversing the latin alphabet (a list of 26 items) 500 times. The results were, more or less:

  • between 2s and 3s with the "3.2 approach" (using append)
  • never above 2s with the "3.3 approach" (using set-nth)

The difference would be even larger with a longer list, simply because swapping indexes is much faster than appending values. So once again, I tried to see if we could get the most of both worlds. Here is what I came up with:

@function reverse($list) {
  // If `set-nth` does exist (Sass 3.3)
  @if function-exists('set-nth') == true {
    @for $i from 1 through floor(length($list) / 2) {
      $list: set-nth(set-nth($list, $i, nth($list, -$i)), -$i, nth($list, $i));
    }

    @return $list;
  }

  // Else it's Sass 3.2
  $result: ();

  @for $i from length($list) * -1 through -1 {
    $result: append($result, nth($list, abs($i)));
  }

  @return $result;
}

There again, we are getting the most of Sass 3.3 while still supporting Sass 3.2. This is pretty neat, don't you think? Of course we could write the function the other way around, dealing with Sass 3.2 first. It makes absolutely no difference whatsoever.

@function reverse($list) {
  // If `set-nth` doesn't exist (Sass 3.2)
  @if function-exists('set-nth') != true {
    $result: ();

    @for $i from length($list) * -1 through -1 {
      $result: append($result, nth($list, abs($i)));
    }

    @return $result;
  }

  // Else it's Sass 3.3
  @for $i from 1 through floor(length($list) / 2) {
    $list: set-nth(set-nth($list, $i, nth($list, -$i)), -$i, nth($list, $i));
  }

  @return $list;
}

Note: to check whether we are running Sass 3.2 in last example, we could have tested function-exists("set-nth") == unquote('function-exists("set-nth")') as well, but that's rather long and error-prone.

Storing the Sass Version in a Variable

To avoid checking for existing features multiple times, and because we only deal with two different Sass versions here, we can store the Sass version in a global variable. Here is how I went about it:

$sass-version: if(function-exists("function-exists") == true, 3.3, 3.2);

I'll give you that's kind of tricky. Allow me to explain what's going on here. We are using the if() ternary function, designed like this:

  • the first argument of the if() function is the condition; it evaluates to true or false
  • if the condition evaluates to true, it returns the second argument
  • else it returns the third argument

Note: Sass 3.2 is kind of buggy with the ternary function. It evaluates all three values, not only the one to be returned. This can sometimes lead to some unexpected errors.

Now, let's have a look at what's going on with Sass 3.3:

  • function-exists('function-exists') returns true because obviously function-exists() exists
  • then function-exists('function-exists') == true is like true == true which is true
  • so $sass-version is set to 3.3

And if we are running Sass 3.2:

  • function-exists('function-exists') is not a function but a string, so basically "function-exists('function-exists')"
  • function-exists('function-exists') == true is false
  • so $sass-version is set to 3.2

If you're a function kind of person, you can wrap this stuff in a function.

@function sass-version() {
  @return if(function-exists("function-exists") == true, 3.3, 3.2);
}

Then use it this way:

@if sass-version() == 3.3 {
  // Sass 3.3
}

@if sass-version() == 3.2 {
  // Sass 3.2
}

@if sass-version() < 3.3 {
  // Sass 3.2
}

Of course, we could have checked the existence of another 3.3 function like call() or map-get() but there could potentially be a version of Sass where *-exists functions are implemented, but not call() or maps, so I feel like it's better to check for the existence of a *-exists function. And since we use function-exists, let's test this one!

To the Future!

Sass 3.3 is the first version to implement *-exists functions, so we have to check whether *-exists($param)actually returns a boolean or is parsed as a string, which is kind of hacky.

Now, let's say Sass 3.4 is being released tomorrow with a unicorn() function, bringing awesomeness and rainbows to the world. The function to detect the Sass version would probably look like this:

@function sass-version() {
  @if function-exists('unicorn') == true {
    @return 3.4;
  }
  @else if function-exists('unicorn') == false {
    @return 3.3;
  }
  @else {
    @return 3.2;
  }
}
Neapolitan Unicorn by Erin Hunting
Neapolitan Unicorn by Erin Hunting

And then if Sass 3.5 brings a rainbow() function, you would update sass-version() this way:

@function sass-version() {
  @if function-exists('rainbow') == true {
    @return 3.5;
  }
  @else if function-exists('unicorn') == true
       and function-exists('rainbow') == false {
    @return 3.4;
  }
  @else if function-exists('unicorn') == false {
    @return 3.3;
  }
  @else {
    @return 3.2;
  }
}

And so on.

Speaking of Unicorns and Rainbows...

What would be really awesome would be the ability to import a file within a conditional statement. Unfortunately, this is not possible at the moment. That being said, it's scheduled for Sass 4.0, so let's not lose hope yet.

Anyway, imagine we could import a file based on the result of the sass-version() function. This would make it darn easy to polyfill Sass 3.3 functions for Sass 3.2.

For instance, we could have a file including all Sass 3.2 versions of map functions using 2-dimensional lists instead (like what Lu Nelson did with Sass-List-Maps) and import it only when dealing with Sass 3.2, like so:

// Unfortunately, this doesn't work :(
@if sass-version() < 3.3 {
  @import "polyfills/maps";
}

Then, we could use all those functions (like map-get) in our code without worrying about the Sass version. Sass 3.3 would use native functions while Sass 3.2 would use polyfills. 

But, that doesn't work.

One could come with the idea of defining functions in a conditional statement, instead of importing a whole file. Then, we could define map-related functions only if they don't exist yet (in other words: Sass 3.2). Unfortunately, this doesn't work either: functions and mixins cannot be defined in a directive.

Functions may not be defined within control directives or other mixins.

The best we can do at the moment is define both a Sass 3.2 and a Sass 3.3 version in each function as we've seen at top of this article. But not only is it more complicated to maintain, but it also requires every Sass 3.3 native function to be wrapped in a custom-named function. Have a look back at our replace-nth function from earlier: we can't name it set-nth(), or it's going to be endlessly recursive when using Sass 3.3. So we have to find a custom name (in this case replace-nth).

Being able to define functions or import files within conditional directives would make it possible to keep native features as is, while generating polyfills for older versions of Sass. Unfortunately, we can't. That sucks.

Meanwhile, I suppose we could use this to warn the user whenever he's using an obsolete Sass compiler. For instance, if your Sass library/framework/whatever is using Sass, you could add this on top of your main stylesheet:

@if sass-version() < 3.3 {
  @warn "You are using a version of Sass prior to 3.3. Unfortunately for you, Sass 3.3 is required for this tool to work. Please make sure to upgrade your Sass compiler.";
}

There. In case the code crashes because you are using unsupported features like maps and stuff, the user will be warned why when he or she checks the output.

Final Thoughts

Until now, Sass has been quite slow to move on versioning stand point. I recall reading somewhere that Sass maintainers were wishing to move on slightly faster, meaning we could find ourselves dealing with multiple Sass versions any time soon.

Learning how to detect the Sass version and make use of *-exists function will — in my opinion — be important one day, at least for some projects (frameworks, grid systems, libraries...). Until then, keep on Sassing guys!

Further Reading

Advertisement